.then().resolve().then() 调用.catch().then() 回调返回的 Promise.then() 的回调中返回 Promise 所需知识:Promise
本章要求您大致熟悉 Promise,但这里也会回顾许多相关知识。如有必要,您可以阅读《JavaScript for impatient programmers》中关于 Promise 的章节。
在本章中,我们将从不同的角度来探讨 Promise:我们不使用这个 API,而是创建一个简单的实现。这种不同的角度曾经极大地帮助我理解了 Promise。
Promise 实现是 ToyPromise 类。为了便于理解,它没有完全匹配 API。但它已经足够接近,仍然可以让我们深入了解 Promise 的工作原理。
包含代码的仓库
ToyPromise 可以在 GitHub 上的 toy-promise 仓库中找到。
我们从 Promise 状态工作原理的简化版本开始(图 11)
v *解析*,它就会变成*已完成*(稍后我们会看到解析也可以拒绝)。v 现在是 Promise 的*完成值*。e *拒绝*,它就会变成*已拒绝*。e 现在是 Promise 的*拒绝值*。我们的第一个实现是一个具有最小功能的独立 Promise
.then() 注册*反应*(回调)。注册必须做正确的事情,而不管 Promise 是否已经 settled。.then() 还不支持链接——它不返回任何东西。ToyPromise1 是一个具有三个原型方法的类
ToyPromise1.prototype.resolve(value)ToyPromise1.prototype.reject(reason)ToyPromise1.prototype.then(onFulfilled, onRejected)也就是说,resolve 和 reject 是方法(而不是传递给构造函数的回调参数的函数)。
以下是第一个实现的使用方法
// .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 的工作原理。但它们是可选的。如果您觉得它们令人困惑,您可以忽略它们,而专注于代码。
ToyPromise1:如果 Promise 被解析,则提供的值将传递给*完成反应*(.then() 的第一个参数)。如果 Promise 被拒绝,则提供的值将传递给*拒绝反应*(.then() 的第二个参数)。.then()我们先来看看 .then()。它必须处理两种情况
onFulfilled 和 onRejected 的调用排队。它们将在以后 Promise settled 时使用。onFulfilled 或 onRejected。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();
}
}前面的代码片段使用了以下辅助函数
Promise 必须始终异步 settled。这就是为什么我们不直接执行任务,而是将它们添加到事件循环(浏览器、Node.js 等)的任务队列中。请注意,真正的 Promise API 不使用普通任务(如 setTimeout()),它使用 *微任务*,微任务与当前的普通任务紧密耦合,并且总是在普通任务之后立即执行。
.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() 类似。
.then() 调用ToyPromise2 链接 .then() 调用:.then() 现在返回一个 Promise,该 Promise 由完成反应或拒绝反应返回的任何值解析。我们实现的下一个功能是链接(图 13):我们从完成反应或拒绝反应返回的值可以由后续 .then() 调用中的完成反应处理。(在下一个版本中,由于对返回 Promise 的特殊支持,链接将变得更加有用。)
在以下示例中
.then():我们在完成反应中返回一个值。.then():我们通过完成反应接收该值。new ToyPromise2()
.resolve('result1')
.then(x => {
assert.equal(x, 'result1');
return 'result2';
})
.then(x => {
assert.equal(x, 'result2');
});在以下示例中
.then():我们在拒绝反应中返回一个值。.then():我们通过完成反应接收该值。new ToyPromise2()
.reject('error1')
.then(null,
x => {
assert.equal(x, 'error1');
return 'result2';
})
.then(x => {
assert.equal(x, 'result2');
});.catch()新版本引入了一个便捷方法 .catch(),它使仅提供拒绝反应变得更加容易。请注意,仅提供完成反应已经很容易——我们只需省略 .then() 的第二个参数(参见前面的示例)。
如果我们使用它(A 行),前面的示例看起来更好
new ToyPromise2()
.reject('error1')
.catch(x => { // (A)
assert.equal(x, 'error1');
return 'result2';
})
.then(x => {
assert.equal(x, 'result2');
});以下两个方法调用是等效的
以下是 .catch() 的实现方式
新版本还将在我们省略完成反应时转发完成,并在我们省略拒绝反应时转发拒绝。为什么这很有用?
以下示例演示了传递拒绝
someAsyncFunction()
.then(fulfillmentReaction1)
.then(fulfillmentReaction2)
.catch(rejectionReaction);rejectionReaction 现在可以处理 someAsyncFunction()、fulfillmentReaction1 和 fulfillmentReaction2 的拒绝。
以下示例演示了传递完成
如果 someAsyncFunction() 拒绝其 Promise,rejectionReaction 可以修复任何错误并返回一个完成值,然后由 fulfillmentReaction 处理。
如果 someAsyncFunction() 完成其 Promise,fulfillmentReaction 也可以处理它,因为 .catch() 被跳过了。
所有这些是如何在幕后处理的?
.then() 返回一个 Promise,该 Promise 由 onFulfilled 或 onRejected 返回的值解析。onFulfilled 或 onRejected,则将它们本应接收到的任何内容传递给 .then() 返回的 Promise。只有 .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(方法的第一行和最后一行)。此外
fulfillmentTask 的工作方式有所不同。这就是完成后的情况onFullfilled,则调用它,并使用其结果来解析 resultPromise。onFulfilled,我们将使用当前 Promise 的完成值来解析 resultPromise。rejectionTask 的工作方式有所不同。这就是拒绝后的情况onRejected,则调用它,并使用其结果来*解析* resultPromise。请注意,resultPromise 没有被拒绝:我们假设 onRejected() 修复了任何问题。onRejected,我们将使用当前 Promise 的拒绝值来拒绝 resultPromise。.then() 回调返回的 Promise.then() 的回调中返回 PromisePromise 扁平化主要是为了使链接更加方便:如果我们想将一个值从一个 .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 本身。
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 直观地显示了新状态。
请注意,*解析*的概念也变得更加复杂。解析 Promise 现在仅意味着它不能再直接 settled 了
ECMAScript 规范这样说:“未解析的 Promise 始终处于待定状态。已解析的 Promise 可能处于待定、已完成或已拒绝状态。”
图 15 显示了 ToyPromise3 如何处理扁平化。
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 锁定在它上面
value 使用结果完成,则当前 Promise 也使用该结果完成。value 因错误被拒绝,则当前 Promise 也因该错误被拒绝。决议是通过私有方法 ._doFulfill() 和 ._doReject() 执行的,以绕过 ._alreadyResolved 的保护。
._doFulfill() 相对简单
_doFulfill(value) { // [new]
assert.ok(!isThenable(value));
this._promiseState = 'fulfilled';
this._promiseResult = value;
this._clearAndEnqueueTasks(this._fulfillmentTasks);
}此处未显示 .reject()。它的唯一新功能是它现在也遵守 ._alreadyResolved。
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 反应 onFulfilled 和 onRejected – 例如
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);
}
}我们跳过了最后一步:如果我们想将 ToyPromise 变成一个实际的 Promise 实现,我们仍然需要实现 揭示构造函数模式:JavaScript Promise 不是通过方法决议和拒绝的,而是通过传递给*执行器*(构造函数的回调参数)的函数来决议和拒绝的。
如果执行器抛出异常,则 promise 被拒绝。