.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 的工作原理。但它们是可选的。如果您觉得它们令人困惑,您可以忽略它们,而专注于代码。
.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()
调用我们实现的下一个功能是链接(图 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
如何处理扁平化。
我们通过以下函数检测 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
。
作为我们的最后一个特性,我们希望我们的 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
被拒绝。