深入理解 JavaScript
请支持本书:购买捐赠
(广告,请不要屏蔽。)

17 通过实现 Promise 来探索它们



  所需知识:Promise

本章要求您大致熟悉 Promise,但这里也会回顾许多相关知识。如有必要,您可以阅读《JavaScript for impatient programmers》中关于 Promise 的章节

在本章中,我们将从不同的角度来探讨 Promise:我们不使用这个 API,而是创建一个简单的实现。这种不同的角度曾经极大地帮助我理解了 Promise。

Promise 实现是 ToyPromise 类。为了便于理解,它没有完全匹配 API。但它已经足够接近,仍然可以让我们深入了解 Promise 的工作原理。

  包含代码的仓库

ToyPromise 可以在 GitHub 上的 toy-promise 仓库中找到。

17.1 回顾:Promise 的状态

图 11:Promise 的状态(简化版):Promise 最初是待定的。如果我们解析它,它就会变成已完成。如果我们拒绝它,它就会变成已拒绝。

我们从 Promise 状态工作原理的简化版本开始(图 11

17.2 版本 1:独立的 Promise

我们的第一个实现是一个具有最小功能的独立 Promise

ToyPromise1 是一个具有三个原型方法的类

也就是说,resolvereject 是方法(而不是传递给构造函数的回调参数的函数)。

以下是第一个实现的使用方法

// .resolve() before .then()
const tp1 = new ToyPromise1();
tp1.resolve('abc');
tp1.then((value) => {
  assert.equal(value, 'abc');
});
// .then() before .resolve()
const tp2 = new ToyPromise1();
tp2.then((value) => {
  assert.equal(value, 'def');
});
tp2.resolve('def');

图 12 说明了我们的第一个 ToyPromise 如何工作。

  Promise 中数据流的图表是可选的

这些图表的目的是直观地解释 Promise 的工作原理。但它们是可选的。如果您觉得它们令人困惑,您可以忽略它们,而专注于代码。

图 12:ToyPromise1:如果 Promise 被解析,则提供的值将传递给*完成反应*(.then() 的第一个参数)。如果 Promise 被拒绝,则提供的值将传递给*拒绝反应*(.then() 的第二个参数)。

17.2.1 方法 .then()

我们先来看看 .then()。它必须处理两种情况

then(onFulfilled, onRejected) {
  const fulfillmentTask = () => {
    if (typeof onFulfilled === 'function') {
      onFulfilled(this._promiseResult);
    }
  };
  const rejectionTask = () => {
    if (typeof onRejected === 'function') {
      onRejected(this._promiseResult);
    }
  };
  switch (this._promiseState) {
    case 'pending':
      this._fulfillmentTasks.push(fulfillmentTask);
      this._rejectionTasks.push(rejectionTask);
      break;
    case 'fulfilled':
      addToTaskQueue(fulfillmentTask);
      break;
    case 'rejected':
      addToTaskQueue(rejectionTask);
      break;
    default:
      throw new Error();
  }
}

前面的代码片段使用了以下辅助函数

function addToTaskQueue(task) {
  setTimeout(task, 0);
}

Promise 必须始终异步 settled。这就是为什么我们不直接执行任务,而是将它们添加到事件循环(浏览器、Node.js 等)的任务队列中。请注意,真正的 Promise API 不使用普通任务(如 setTimeout()),它使用 *微任务*,微任务与当前的普通任务紧密耦合,并且总是在普通任务之后立即执行。

17.2.2 方法 .resolve()

.resolve() 的工作原理如下:如果 Promise 已经 settled,它什么也不做(确保 Promise 只能 settled 一次)。否则,Promise 的状态将更改为 'fulfilled',结果将缓存在 this.promiseResult 中。接下来,调用到目前为止已排队的 所有完成反应。

resolve(value) {
  if (this._promiseState !== 'pending') return this;
  this._promiseState = 'fulfilled';
  this._promiseResult = value;
  this._clearAndEnqueueTasks(this._fulfillmentTasks);
  return this; // enable chaining
}
_clearAndEnqueueTasks(tasks) {
  this._fulfillmentTasks = undefined;
  this._rejectionTasks = undefined;
  tasks.map(addToTaskQueue);
}

reject()resolve() 类似。

17.3 版本 2:链接 .then() 调用

图 13:ToyPromise2 链接 .then() 调用:.then() 现在返回一个 Promise,该 Promise 由完成反应或拒绝反应返回的任何值解析。

我们实现的下一个功能是链接(图 13):我们从完成反应或拒绝反应返回的值可以由后续 .then() 调用中的完成反应处理。(在下一个版本中,由于对返回 Promise 的特殊支持,链接将变得更加有用。)

在以下示例中

new ToyPromise2()
  .resolve('result1')
  .then(x => {
    assert.equal(x, 'result1');
    return 'result2';
  })
  .then(x => {
    assert.equal(x, 'result2');
  });

在以下示例中

new ToyPromise2()
  .reject('error1')
  .then(null,
    x => {
      assert.equal(x, 'error1');
      return 'result2';
    })
  .then(x => {
    assert.equal(x, 'result2');
  });

17.4 便捷方法 .catch()

新版本引入了一个便捷方法 .catch(),它使仅提供拒绝反应变得更加容易。请注意,仅提供完成反应已经很容易——我们只需省略 .then() 的第二个参数(参见前面的示例)。

如果我们使用它(A 行),前面的示例看起来更好

new ToyPromise2()
  .reject('error1')
  .catch(x => { // (A)
    assert.equal(x, 'error1');
    return 'result2';
  })
  .then(x => {
    assert.equal(x, 'result2');
  });

以下两个方法调用是等效的

.catch(rejectionReaction)
.then(null, rejectionReaction)

以下是 .catch() 的实现方式

catch(onRejected) { // [new]
  return this.then(null, onRejected);
}

17.5 省略反应

新版本还将在我们省略完成反应时转发完成,并在我们省略拒绝反应时转发拒绝。为什么这很有用?

以下示例演示了传递拒绝

someAsyncFunction()
  .then(fulfillmentReaction1)
  .then(fulfillmentReaction2)
  .catch(rejectionReaction);

rejectionReaction 现在可以处理 someAsyncFunction()fulfillmentReaction1fulfillmentReaction2 的拒绝。

以下示例演示了传递完成

someAsyncFunction()
  .catch(rejectionReaction)
  .then(fulfillmentReaction);

如果 someAsyncFunction() 拒绝其 Promise,rejectionReaction 可以修复任何错误并返回一个完成值,然后由 fulfillmentReaction 处理。

如果 someAsyncFunction() 完成其 Promise,fulfillmentReaction 也可以处理它,因为 .catch() 被跳过了。

17.6 实现

所有这些是如何在幕后处理的?

只有 .then() 发生了变化

then(onFulfilled, onRejected) {
  const resultPromise = new ToyPromise2(); // [new]

  const fulfillmentTask = () => {
    if (typeof onFulfilled === 'function') {
      const returned = onFulfilled(this._promiseResult);
      resultPromise.resolve(returned); // [new]
    } else { // [new]
      // `onFulfilled` is missing
      // => we must pass on the fulfillment value
      resultPromise.resolve(this._promiseResult);
    }  
  };

  const rejectionTask = () => {
    if (typeof onRejected === 'function') {
      const returned = onRejected(this._promiseResult);
      resultPromise.resolve(returned); // [new]
    } else { // [new]
      // `onRejected` is missing
      // => we must pass on the rejection value
      resultPromise.reject(this._promiseResult);
    }
  };

  ···

  return resultPromise; // [new]
}

.then() 创建并返回一个新的 Promise(方法的第一行和最后一行)。此外

17.7 版本 3:扁平化从 .then() 回调返回的 Promise

17.7.1 从 .then() 的回调中返回 Promise

Promise 扁平化主要是为了使链接更加方便:如果我们想将一个值从一个 .then() 回调传递给下一个,我们在前一个回调中返回它。之后,.then() 将其放入它已经返回的 Promise 中。

如果我们从 .then() 回调返回一个 Promise,这种方法就会变得不方便。例如,基于 Promise 的函数的结果(A 行)

asyncFunc1()
.then((result1) => {
  assert.equal(result1, 'Result of asyncFunc1()');
  return asyncFunc2(); // (A)
})
.then((result2Promise) => {
  result2Promise
  .then((result2) => { // (B)
    assert.equal(
      result2, 'Result of asyncFunc2()');
  });
});

这一次,将 A 行返回的值放入 .then() 返回的 Promise 中,迫使我们在 B 行解包该 Promise。如果相反,A 行返回的 Promise 替换了 .then() 返回的 Promise,那就太好了。如何做到这一点尚不清楚,但如果可行,它将让我们像这样编写代码

asyncFunc1()
.then((result1) => {
  assert.equal(result1, 'Result of asyncFunc1()');
  return asyncFunc2(); // (A)
})
.then((result2) => {
  // result2 is the fulfillment value, not the Promise
  assert.equal(
    result2, 'Result of asyncFunc2()');
});

在 A 行,我们返回了一个 Promise。由于 Promise 扁平化,result2 是该 Promise 的完成值,而不是 Promise 本身。

17.7.2 扁平化使 Promise 状态更加复杂

  ECMAScript 规范中的 Promise 扁平化

在 ECMAScript 规范中,Promise 扁平化的细节在 “Promise 对象”部分 中描述。

Promise API 如何处理扁平化?

如果 Promise P 用 Promise Q 解析,则 P 不会包装 Q,P“变成”Q:P 的状态和 settled 值现在始终与 Q 相同。这有助于我们使用 .then(),因为 .then() 使用其回调之一返回的值来解析它返回的 Promise。

P 如何变成 Q?通过*锁定*Q:P 变得无法从外部解析,并且 Q 的 settled 会触发 P 的 settled。锁定是一种额外的不可见 Promise 状态,它使状态更加复杂。

Promise API 还有一个额外的功能:Q 不必是 Promise,只需是所谓的*可 thenable 的*。可 thenable 的对象是一个具有 .then() 方法的对象。这种额外灵活性的原因是使不同的 Promise 实现能够协同工作(这在 Promise 首次添加到语言中时很重要)。

图 14 直观地显示了新状态。

图 14:Promise 的所有状态:Promise 扁平化引入了不可见的伪状态“已锁定”。如果 Promise P 用可 thenable 的 Q 解析,则会达到该状态。之后,P 的状态和 settled 值始终与 Q 相同。

请注意,*解析*的概念也变得更加复杂。解析 Promise 现在仅意味着它不能再直接 settled 了

ECMAScript 规范这样说:“未解析的 Promise 始终处于待定状态。已解析的 Promise 可能处于待定、已完成或已拒绝状态。”

17.7.3 实现 Promise 扁平化

图 15 显示了 ToyPromise3 如何处理扁平化。

图 15:ToyPromise3 会扁平化已决议的 Promise:如果第一个 Promise 使用 thenable x1 决议,它会锁定在 x1 上,并使用 x1 的决议值进行决议。如果第一个 Promise 使用非 thenable 值决议,则一切工作方式与以前相同。

我们通过以下函数检测 thenable

function isThenable(value) { // [new]
  return typeof value === 'object' && value !== null
    && typeof value.then === 'function';
}

为了实现锁定,我们引入了一个新的布尔标志 ._alreadyResolved。将其设置为 true 会停用 .resolve().reject() – 例如

resolve(value) { // [new]
  if (this._alreadyResolved) return this;
  this._alreadyResolved = true;

  if (isThenable(value)) {
    // Forward fulfillments and rejections from `value` to `this`.
    // The callbacks are always executed asynchronously
    value.then(
      (result) => this._doFulfill(result),
      (error) => this._doReject(error));
  } else {
    this._doFulfill(value);
  }

  return this; // enable chaining
}

如果 value 是 thenable,则我们将当前 Promise 锁定在它上面

决议是通过私有方法 ._doFulfill()._doReject() 执行的,以绕过 ._alreadyResolved 的保护。

._doFulfill() 相对简单

_doFulfill(value) { // [new]
  assert.ok(!isThenable(value));
  this._promiseState = 'fulfilled';
  this._promiseResult = value;
  this._clearAndEnqueueTasks(this._fulfillmentTasks);
}

此处未显示 .reject()。它的唯一新功能是它现在也遵守 ._alreadyResolved

17.8 版本 4:反应回调中抛出的异常

图 16:ToyPromise4 将 Promise 反应中的异常转换为 .then() 返回的 Promise 的拒绝。

作为我们的最后一个特性,我们希望我们的 Promise 将用户代码中的异常作为拒绝处理(图 16)。在本章中,“用户代码”是指 .then() 的两个回调参数。

new ToyPromise4()
  .resolve('a')
  .then((value) => {
    assert.equal(value, 'a');
    throw 'b'; // triggers a rejection
  })
  .catch((error) => {
    assert.equal(error, 'b');
  })

.then() 现在通过辅助方法 ._runReactionSafely() 安全地运行 Promise 反应 onFulfilledonRejected – 例如

  const fulfillmentTask = () => {
    if (typeof onFulfilled === 'function') {
      this._runReactionSafely(resultPromise, onFulfilled); // [new]
    } else {
      // `onFulfilled` is missing
      // => we must pass on the fulfillment value
      resultPromise.resolve(this._promiseResult);
    }  
  };

._runReactionSafely() 的实现如下

_runReactionSafely(resultPromise, reaction) { // [new]
  try {
    const returned = reaction(this._promiseResult);
    resultPromise.resolve(returned);
  } catch (e) {
    resultPromise.reject(e);
  }
}

17.9 版本 5:揭示构造函数模式

我们跳过了最后一步:如果我们想将 ToyPromise 变成一个实际的 Promise 实现,我们仍然需要实现 揭示构造函数模式:JavaScript Promise 不是通过方法决议和拒绝的,而是通过传递给*执行器*(构造函数的回调参数)的函数来决议和拒绝的。

const promise = new Promise(
  (resolve, reject) => { // executor
    // ···
  });

如果执行器抛出异常,则 promise 被拒绝。