Javascript Promisification

2023-02-17 10:53 更新

對于一個(gè)簡單的轉(zhuǎn)換來說 “Promisification” 是一個(gè)長單詞。它指將一個(gè)接受回調(diào)的函數(shù)轉(zhuǎn)換為一個(gè)返回 promise 的函數(shù)。

由于許多函數(shù)和庫都是基于回調(diào)的,因此,在實(shí)際開發(fā)中經(jīng)常會(huì)需要進(jìn)行這種轉(zhuǎn)換。因?yàn)槭褂?promise 更加方便,所以將基于回調(diào)的函數(shù)和庫 promise 化是有意義的。

為了更好地理解,讓我們來看一個(gè)例子。

例如,在 簡介:回調(diào) 一章中我們有 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);
}

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

該函數(shù)通過給定的 src 加載腳本,然后在出現(xiàn)錯(cuò)誤時(shí)調(diào)用 callback(err),或者在加載成功時(shí)調(diào)用 callback(null, script)。這是大家對于使用回調(diào)函數(shù)的共識,我們之前也學(xué)習(xí)過。

現(xiàn)在,讓我們將其 promise 化吧。

我們將創(chuàng)建一個(gè)新的函數(shù) loadScriptPromise(src),與上面的函數(shù)作用相同(加載腳本),只是我們創(chuàng)建的這個(gè)函數(shù)會(huì)返回一個(gè) promise 而不是使用回調(diào)。

換句話說,我們僅向它傳入 src(沒有 callback)并通過該函數(shù)的 return 獲得一個(gè) promise,當(dāng)腳本加載成功時(shí),該 promise 將以 script 為結(jié)果 resolve,否則將以出現(xiàn)的 error 為結(jié)果 reject。

代碼實(shí)現(xiàn)如下:

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

// 用法:
// loadScriptPromise('path/script.js').then(...)

正如我們所看到的,新的函數(shù)是對原始的 loadScript 函數(shù)的包裝。新函數(shù)調(diào)用它,并提供了自己的回調(diào)來將其轉(zhuǎn)換成 promise resolve/reject。

現(xiàn)在 loadScriptPromise 非常適用于基于 promise 的代碼了。如果我們相比于回調(diào)函數(shù),更喜歡 promise(稍后我們將看到更多喜歡 promise 的原因),那么我們將改用它。

在實(shí)際開發(fā)中,我們可能需要 promise 化很多函數(shù),所以使用一個(gè) helper(輔助函數(shù))很有意義。

我們將其稱為 promisify(f):它接受一個(gè)需要被 promise 化的函數(shù) f,并返回一個(gè)包裝(wrapper)函數(shù)。

function promisify(f) {
  return function (...args) { // 返回一個(gè)包裝函數(shù)(wrapper-function) (*)
    return new Promise((resolve, reject) => {
      function callback(err, result) { // 我們對 f 的自定義的回調(diào) (**)
        if (err) {
          reject(err);
        } else {
          resolve(result);
        }
      }

      args.push(callback); // 將我們的自定義的回調(diào)附加到 f 參數(shù)(arguments)的末尾

      f.call(this, ...args); // 調(diào)用原始的函數(shù)
    });
  };
}

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

代碼看起來可能有些復(fù)雜,但其本質(zhì)與我們在上面寫的那個(gè)是一樣的,就是將 loadScript 函數(shù) promise 化。

調(diào)用 promisify(f) 會(huì)返回一個(gè) f (*) 的包裝器。該包裝器返回一個(gè) promise,并將調(diào)用轉(zhuǎn)發(fā)給原始的 f,并在我們自定義的回調(diào) (**) 中跟蹤結(jié)果。

在這里,promisify 假設(shè)原始函數(shù)期望一個(gè)帶有兩個(gè)參數(shù) (err, result) 的回調(diào)。這就是我們最常遇到的形式。那么我們自定義的回調(diào)的格式是完全正確的,在這種情況下 promisify 也可以完美地運(yùn)行。

但是如果原始的 f 期望一個(gè)帶有更多參數(shù)的回調(diào) callback(err, res1, res2, ...),該怎么辦呢?

我們可以繼續(xù)改進(jìn)我們的輔助函數(shù)。讓我們寫一個(gè)更高階版本的 promisify。

  • 當(dāng)它被以 ?promisify(f)? 的形式調(diào)用時(shí),它應(yīng)該以與上面那個(gè)版本的實(shí)現(xiàn)的工作方式類似。
  • 當(dāng)它被以 ?promisify(f, true)? 的形式調(diào)用時(shí),它應(yīng)該返回以回調(diào)函數(shù)數(shù)組為結(jié)果 resolve 的 promise。這就是具有很多個(gè)參數(shù)的回調(diào)的結(jié)果。
// promisify(f, true) 來獲取結(jié)果數(shù)組
function promisify(f, manyArgs = false) {
  return function (...args) {
    return new Promise((resolve, reject) => {
      function callback(err, ...results) { // 我們自定義的 f 的回調(diào)
        if (err) {
          reject(err);
        } else {
          // 如果 manyArgs 被指定,則使用所有回調(diào)的結(jié)果 resolve
          resolve(manyArgs ? results : results[0]);
        }
      }

      args.push(callback);

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

// 用法:
f = promisify(f, true);
f(...).then(arrayOfResults => ..., err => ...);// promisify(f, true) 來獲取結(jié)果數(shù)組
function promisify(f, manyArgs = false) {
  return function (...args) {
    return new Promise((resolve, reject) => {
      function callback(err, ...results) { // 我們自定義的 f 的回調(diào)
        if (err) {
          reject(err);
        } else {
          // 如果 manyArgs 被指定,則使用所有回調(diào)的結(jié)果 resolve
          resolve(manyArgs ? results : results[0]);
        }
      }

      args.push(callback);

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

// 用法:
f = promisify(f, true);
f(...).then(arrayOfResults => ..., err => ...);// promisify(f, true) 來獲取結(jié)果數(shù)組
function promisify(f, manyArgs = false) {
  return function (...args) {
    return new Promise((resolve, reject) => {
      function callback(err, ...results) { // 我們自定義的 f 的回調(diào)
        if (err) {
          reject(err);
        } else {
          // 如果 manyArgs 被指定,則使用所有回調(diào)的結(jié)果 resolve
          resolve(manyArgs ? results : results[0]);
        }
      }

      args.push(callback);

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

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

正如你所看到的,它與上面那個(gè)實(shí)現(xiàn)基本相同,只是根據(jù) manyArgs 是否為真來決定僅使用一個(gè)還是所有參數(shù)調(diào)用 resolve。

對于一些更奇特的回調(diào)格式,例如根本沒有 err 的格式:callback(result),我們可以手動(dòng) promise 化這樣的函數(shù),而不使用 helper。

也有一些具有更靈活一點(diǎn)的 promisification 函數(shù)的模塊(module),例如 es6-promisify。在 Node.js 中,有一個(gè)內(nèi)建的 promise 化函數(shù) util.promisify。

請注意:

Promisification 是一種很好的方法,特別是在你使用 async/await 的時(shí)候(請看下一章),但不是回調(diào)的完全替代。

請記住,一個(gè) promise 可能只有一個(gè)結(jié)果,但從技術(shù)上講,一個(gè)回調(diào)可能被調(diào)用很多次。

因此,promisification 僅適用于調(diào)用一次回調(diào)的函數(shù)。進(jìn)一步的調(diào)用將被忽略。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號