2022 年 10 月 18 日

承諾化

「承諾化」是一個很長的字,用來表示一個簡單的轉換。它將接受回呼的函式轉換成回傳承諾的函式。

在現實生活中,通常需要進行這樣的轉換,因為許多函式和函式庫都是基於回呼的。但是承諾比較方便,所以將它們承諾化是有意義的。

為了更深入地了解,我們來看一個範例。

例如,我們有章節 簡介:回呼 中的 loadScript(src, callback)

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;

  script.onload = () => callback(null, script);
  script.onerror = () => callback(new Error(`Script load error for ${src}`));

  document.head.append(script);
}

// usage:
// loadScript('path/script.js', (err, script) => {...})

此函式會載入具有給定 src 的腳本,然後在發生錯誤時呼叫 callback(err),或是在載入成功時呼叫 callback(null, script)。這是使用回呼的廣泛共識,我們之前看過。

讓我們將它承諾化。

我們將建立一個新的函式 loadScriptPromise(src),它執行相同的動作(載入腳本),但會回傳承諾,而不是使用回呼。

換句話說,我們只傳遞 src(沒有 callback)給它,並取得一個承諾作為回傳,在載入成功時會以 script 解決,否則會以錯誤拒絕。

在這裡

let loadScriptPromise = function(src) {
  return new Promise((resolve, reject) => {
    loadScript(src, (err, script) => {
      if (err) reject(err);
      else resolve(script);
    });
  });
};

// usage:
// loadScriptPromise('path/script.js').then(...)

正如我們所見,新函式是原始 loadScript 函式的包裝器。它呼叫它,提供它自己的回呼,轉換為承諾 resolve/reject

現在 loadScriptPromise 非常適合基於承諾的程式碼。如果我們比回呼更喜歡承諾(我們很快就會看到更多原因),那麼我們將使用它。

在實務上,我們可能需要將多個函式承諾化,因此使用輔助程式是有意義的。

我們將呼叫它 promisify(f):它接受要承諾化的函式 f,並傳回包裝函式。

function promisify(f) {
  return function (...args) { // return a wrapper-function (*)
    return new Promise((resolve, reject) => {
      function callback(err, result) { // our custom callback for f (**)
        if (err) {
          reject(err);
        } else {
          resolve(result);
        }
      }

      args.push(callback); // append our custom callback to the end of f arguments

      f.call(this, ...args); // call the original function
    });
  };
}

// usage:
let loadScriptPromise = promisify(loadScript);
loadScriptPromise(...).then(...);

程式碼看起來可能有點複雜,但它基本上與我們在上面承諾化 loadScript 函式時寫的一樣。

呼叫 promisify(f) 會傳回 f (*) 的包裝器。該包裝器傳回承諾,並將呼叫轉發到原始 f,在自訂回呼中追蹤結果 (**)

在這裡,promisify 假設原始函式預期回呼有兩個參數 (err, result)。這是我們最常遇到的情況。然後,我們的自訂回呼格式完全正確,而 promisify 非常適合這種情況。

但是,如果原始 f 預期回呼有更多參數 callback(err, res1, res2, ...) 呢?

我們可以改善我們的輔助程式。讓我們製作更進階版本的 promisify

  • 當呼叫為 promisify(f) 時,它應與上述版本類似。
  • 當呼叫為 promisify(f, true) 時,它應傳回承諾,並以回呼結果陣列解析。這完全適用於具有多個參數的回呼。
// promisify(f, true) to get array of results
function promisify(f, manyArgs = false) {
  return function (...args) {
    return new Promise((resolve, reject) => {
      function callback(err, ...results) { // our custom callback for f
        if (err) {
          reject(err);
        } else {
          // resolve with all callback results if manyArgs is specified
          resolve(manyArgs ? results : results[0]);
        }
      }

      args.push(callback);

      f.call(this, ...args);
    });
  };
}

// usage:
f = promisify(f, true);
f(...).then(arrayOfResults => ..., err => ...);

正如您所見,它基本上與上述相同,但 resolve 僅呼叫一個或所有參數,具體取決於 manyArgs 是否為真。

對於更特別的回呼格式,例如完全沒有 err 的格式:callback(result),我們可以手動承諾化此類函式,而不需要使用輔助程式。

還有一些模組具有更靈活的承諾化函式,例如 es6-promisify。在 Node.js 中,有一個內建的 util.promisify 函式。

請注意

承諾化是一個很棒的方法,特別是當您使用 async/await(稍後在章節 Async/await 中介紹)時,但並非完全取代回呼。

請記住,承諾可能只有一個結果,但回呼技術上可以呼叫多次。

所以 promisification 僅適用於呼叫一次 callback 的函式。後續呼叫將會被忽略。

教學課程地圖

留言

留言前請先閱讀…
  • 如果你有改善建議 - 請 提交 GitHub issue 或發起 pull request,而非留言。
  • 如果你看不懂文章中的某個部分 – 請詳細說明。
  • 要插入一些程式碼,請使用 <code> 標籤,對於多行程式碼 – 請用 <pre> 標籤包覆,對於超過 10 行的程式碼 – 請使用沙盒 (plnkrjsbincodepen…)