2022 年 10 月 18 日

Promisification

“Promisification”是一个长单词,表示一个简单的转换。它是将接受回调的函数转换为返回 Promise 的函数。

这种转换在现实生活中经常需要,因为许多函数和库都是基于回调的。但 Promise 更方便,因此将它们 promisify 是有意义的。

为了更好地理解,我们来看一个例子。

例如,我们有来自章节 介绍:回调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)。这是使用回调的广泛约定,我们之前已经见过。

让我们将它 promisify。

我们将创建一个新函数loadScriptPromise(src),它执行相同操作(加载脚本),但返回一个 Promise 而不是使用回调。

换句话说,我们只传递 src(没有 callback),并获得一个 promise,如果加载成功,则使用 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 函数的包装器。它调用它,提供自己的回调,该回调转换为 promise resolve/reject

现在 loadScriptPromise 非常适合基于 promise 的代码。如果我们更喜欢 promise 而不是回调(我们很快就会看到更多原因),那么我们将使用它。

在实践中,我们可能需要对多个函数进行 promisify,因此使用助手是有意义的。

我们将称之为 promisify(f):它接受要 promisify 的函数 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。

promisify(f) 的调用返回 f (*) 周围的包装器。该包装器返回一个 promise,并将调用转发到原始 f,在自定义回调 (**) 中跟踪结果。

在这里,promisify 假设原始函数期望具有恰好两个参数 (err, result) 的回调。这是我们最常遇到的情况。然后,我们的自定义回调完全采用正确的格式,并且 promisify 非常适合这种情况。

但是,如果原始 f 期望具有更多参数的回调 callback(err, res1, res2, ...) 呢?

我们可以改进我们的助手。让我们制作一个更高级版本的 promisify

  • 当作为 promisify(f) 调用时,它应该与上述版本类似。
  • 当作为 promisify(f, true) 调用时,它应该返回解析为回调结果数组的 promise。这完全适用于具有许多参数的回调。
// 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),我们可以手动对这些函数进行 promisify,而无需使用助手。

还有一些模块具有更灵活的 promisify 函数,例如 es6-promisify。在 Node.js 中,有一个内置的 util.promisify 函数。

请注意

Promise 化是一种很好的方法,尤其是在使用 async/await(本章后面会介绍 Async/await)时,但它并不能完全替代回调。

请记住,一个 Promise 可能只有一个结果,但一个回调在技术上可以被调用多次。

因此,Promise 化只适用于那些只调用回调一次的函数。后续的调用将被忽略。

教程地图

评论

在评论之前请阅读此内容…
  • 如果您有改进建议 - 请 提交 GitHub 问题或提交拉取请求,而不是发表评论。
  • 如果您无法理解文章中的某些内容 - 请详细说明。
  • 要插入几行代码,请使用 <code> 标记,要插入多行代码,请将其包装在 <pre> 标记中,要插入 10 行以上的代码,请使用沙盒(plnkrjsbincodepen…)