本文为《人人都能读标准》—— ECMAScript篇的第18篇。我在这个仓库中系统地介绍了标准的阅读规则以及使用方式,并深入剖析了标准对JavaScript核心原理的描述。
Promise是JavaScript中的“明星对象”。如果你在github搜一搜,你会发现有大量的程序员在造Promise的轮子,与之相比,你就几乎看不到有人会说“我要手写一个Math对象”。
我自己也造一个Promise的轮子,不过这个“轮子”跟其他人的“轮子”不太一样,这个轮子可以通过test262标准符合性测试99.3%的测试用例。在同类产品中,then/Promise只有63.8%的通过率,es6-promise只有46%的通过率。你可以在spec-promise中看到我轮子的源码,以及具体的测试方法。
我是如何做到这一点的?是靠个人聪明才智么?那肯定不是。论能力,目前的我肯定远远不及这些项目背后的大佬。我使用的是看起来“非常笨”的方法:跟着标准定义的Promise按部就班地实现。
本节的内容会分为两个部分,前面的部分我会为你展示基于标准的定义实现Promise的大致过程。基于易读性的考虑,我不能把spec-promise中的代码直接贴进来,我会使用更为简易的代码框架为你展示实现的过程。当然,简易不是简陋,最后这个Promise还是可以小跑一下的,完整的代码可见这里。而剩余的部分,我会延续16.生成器中异步编程的内容,从标准的角度为你展示,Async函数是如何实现异步编程的。
跟着标准写Promise
Promise骨架
在1.阅读向导我提到过,标准在定义每一个内置构造器的时候,都会从构造器函数、构造器属性、prototype对象属性、实例属性4个角度出发。基于这4个角度,我们可以从标准Promise章节的目录看到Promise的“架构”:
于是,我们就有了Promise类的JavaScript骨架:
class Promise {
// 27.2.3 The Promise Constructor: 构造函数
constructor(executor){}
// 27.2.4 Properties of the Promise Constructor:构造器属性
static all(iterable){}
static allSettled(iterable){}
static any(iterable){}
static race(iterable){}
static reject(r){}
static resolve(x){}
static get [Symbol.species](){}
// 27.2.5 Properties of the Promise Prototype Object:prototype对象属性
then(onFulfilled, onRejected){}
catch(onRejected){}
finally(onFinally){}
[Symbol.toStringTag](){}
// 27.2.6 Properties of Promise Instances:实例属性(内部插槽)
__PromiseState;
__PromiseResult;
__PromiseFulfillReactions;
__PromiseRejectReactions;
__PromiseIsHandled;
}
值得注意的是,这里的实例属性都是Promise实例对象中的内部插槽,理论上来说是无法被代码观察到的。但由于我们的Promise使用JavaScript实现,所以只能使用带有__前缀的属性名表示内部插槽。(不使用私有属性#是因为在后续的实现中,外部的“抽象操作”也会用到这些实例属性)。
这些内部插槽的含义我用下表给你总结:
| 内部插槽 | 描述 |
|---|---|
| __PromiseState | 表示Promise的状态:pending、fulfilled、rejected。 |
| __PromiseResult | 表示Promise的结果。 |
| __PromiseFulfillReactions | fulfilled状态的handlers列表。当状态从pending转为fulfilled时,列表中的handler会依次触发。 |
| __PromiseRejectReactions | rejected状态的handlers列表。当状态从pending转为rejected时,列表中的handler会依次触发。 |
| __PromiseIsHandled | 记录promise是否注册了handler。 |
构造函数constructor
constructor方法会在new表达式上被触发。
从标准27.2.3 Promise构造函数的定义可以看出,该算法主要做以下几个事情:
- 判断方法是否被合法调用:必须由new表达式或super方法触发,且传入的第一个参数executor必须是一个函数;
- 创建promise实例对象,并初始化其内部插槽;
- 创建resolve,reject两个内置函数;
- 分别以resolve,reject作为executor的参数,执行executor;如果executor的执行有误,直接reject promise。
- 返回promise对象。
翻译成代码是这样的:
class Promise {
constructor(executor) {
// 1. 判断方法是否被合法调用:必须由new表达式、super方法触发,且传入的第一个参数executor必须是一个函数。
if (!new.target) throw new TypeError("Promise constructor cannot be invoked without 'new'")
if (typeof executor !== 'function') throw new TypeError("Promise resolver is not a function")
// 2. 创建promise实例对象,并初始化其内部插槽
const promise = Object.create(Promise.prototype)
promise.__PromiseState = "pending"
promise.__PromiseFulfillReactions = []
promise.__PromiseRejectReactions = []
promise.__PromiseIsHandled = false
// 3. 创建resolve,reject两个内置函数
let { resolve, reject } = createResolvingFunction(promise)
// 4. 分别以resolve,reject作为executor的参数,执行executor;如果executor的执行有误,直接reject promise。
try {
executor.call(undefined, resolve, reject)
} catch (e) {
reject.call(undefined, e)
}
// 5. 返回promise对象。
return promise
}
// ...
}
在第3步中,createResolvingFunction是一个抽象操作,这个抽象操作会创建resolve()、reject()两个内置函数,调用它们会将promise的状态从pending分别转为fulfilled、rejected,并触发对应状态handler列表上的函数,如下面的代码所示:
function createResolvingFunction(promise){
let alreadyResolve = false // 使得resolve、reject只允许触发一次
return {
resolve: function (value) { // resolve内置函数
if (alreadyResolve) return undefined;
alreadyResolve = true
// 修改内部插槽
let reactions = promise.__PromiseFulfillReactions
promise.__PromiseState = "fulfilled"
promise.__PromiseFulfillReactions = undefined
promise.__PromiseRejectReactions = undefined
promise.__PromiseResult = value
// 触发handler
for (let reaction of reactions) {
queueMicrotask(reaction.bind(undefined, value))
}
},
reject: function (reason) { // reject内置函数
if (alreadyResolve) return undefined;
alreadyResolve = true
// 修改内部插槽
let reactions = promise.__PromiseRejectReactions
promise.__PromiseState = "rejected"
promise.__PromiseFulfillReactions = undefined
promise.__PromiseRejectReactions = undefined
promise.__PromiseResult = reason
// 触发handler
for (let reaction of reactions) {
queueMicrotask(reaction.bind(undefined, reason))
}
}
}
}
当然,以上并不是createResolvingFunction的所有逻辑,比如,当resolve()的参数是一个promise时,会有其他的逻辑。你可以在标准中看到这个抽象操作的完整逻辑,也可以spec-promise的实现。
prototype对象的方法
不管是prototype对象的方法还是静态方法,在执行的过程中,往往会创建新的promise,于是在使用自然语言解释这个过程时,常常很容易导致读者混淆,所以这里需要先做一些用词上的规定。后续的篇幅都会使用以下的词汇区分promise对象:
- old_promise:表示被触发方法的promise对象;
- new_promise:表示方法执行过程中创建的新的promise对象。
Promise的prototype对象最重要的方法就是then方法,很多其他的prototype对象方法、静态方法、甚至await语句都是基于Promise.prototype.then实现的。
then方法的核心逻辑其实也很简单,它接受两个函数(onFulfilled,onRejected)作为参数,主要做以下两件事情:
- 创建一个新的promise(
new_Promise),并分别对onFulfilled以及onRejected再做一层封装,得到两个新的函数:onFulfilledJobCallback:用以触发onFulfilled的逻辑,然后根据执行过程中有无错误选择resolve/rejectnew_Promise;onRejectedJobCallback:用以触发onRejected的逻辑,然后根据执行过程中有无错误选择resolve/rejectnew_Promise;;
- 根据
old_promise的状态做不同的操作:- 如果是
pending:- 把
onFulfilledJobCallback添加到old_promise的fulfilled handlers列表(__PromiseFulfillReactions)中 - 把
onRejectedJobCallback添加到old_promise的rejected handlers列表(__PromiseRejectReactions)中。
- 把
- 如果是
fulfilled,以old_promise.__PromiseResult为参数,把onFulfilledJobCallback添加到宿主微任务队列。 - 如果是
rejected,以old_promise.__PromiseResult为参数,把onRejectedJobCallback添加到宿主微任务队列。
- 如果是
把这个逻辑翻译成代码大概是这样的:
class Promise {
// ...
then(onFulfilled, onRejected) {
return new Promise((resolve, reject) => {
// 封装onFufilled
let onFulfilledJobCallback = function (value) {
try {
let handerResult = onFulfilled.call(undefined, value);
resolve(handerResult)
} catch (e) {
reject(e)
}
}
// 封装onRejected
let onRejectedJobCallback = function (reason) {
try {
let handerResult = onRejected.call(undefined, reason);
resolve(handerResult)
} catch (e) {
reject(e)
}
}
// 根据old_promise的状态做不同的操作:
if (this.__PromiseState === "pending") {
this.__PromiseFulfillReactions.push(onFulfilledJobCallback)
this.__PromiseRejectReactions.push(onRejectedJobCallback)
} else if (this.__PromiseState === "fulfilled") {
queueMicrotask(onFulfilledJobCallback.bind(undefined, this.__PromiseResult))
} else if (this.__PromiseState === "rejected") {
queueMicrotask(onRejectedJobCallback.bind(undefined, this.__PromiseResult))
}
})
}
// ...
}
还记得我们在3.宿主环境中说的吗,标准使用HostEnqueuePromiseJob表示注册promise微任务的过程。而我在这里使用queueMicrotask替代这个抽象操作。以防你不知道,这是一个不管在浏览器宿主还是在node环境中都真实可用的方法。
当然,这里其实省略了一些边缘逻辑。比如,对参数onFulfilled/onRejected不是函数类型的情况做处理、对执行onFulfilled/onRejected时Realm的选择等等。完整的逻辑可见标准,也可见spec-promise。
有了then方法,catch方法的逻辑就非常简单了:
class Promise {
// ...
catch(onRejected){
return this.then(undefined, onRejected)
}
}
finally方法也是完全基于then方法实现的,但是finally会对传入的函数onFinally先做一层封装:
class Promise {
// ...
finally(onFinally){
// ... 省略对edge cases的处理
let thenFinally = function(value){
let result = onFinally.call(undefined) // ①
return Promise.resolve(result).then(() => {return value}) // ②
}
let catchFinally = function(reason){
let result = onFinally.call(undefined) // ①
return Promise.resolve(result).then(() => {throw reason}) // ②
}
return this.then(thenFinally, catchFinally) // ③
}
}
从这段逻辑,你可以看出finally方法的一些特点:
-
参数
onFinally函数在被调用时,不会传入任何参数(注释①):Promise.resolve("first").finally((arg) => {console.log(arg)}) // undefined -
对finally方法进行链式调用时,finally不会修改链上传递的值(注释②):
Promise.resolve("first").finally(() => {return "second"}).then((arg) => console.log(arg)) // "first" -
在finally方法的执行过程中,实际上调用了两次then方法:第一次是在返回语句中(注释③),第二次是在执行thenFinally/catchFinally的过程中(注释②)。于是,完整执行一次finally,至少需要两次执行微任务队列,所以你会看到下面代码的这种奇异行为:
Promise.resolve().finally(() => {}).then(() => {console.log("first")}) Promise.resolve().then(() => {}).then(() => {console.log("second")}) // sencond // first
静态方法
Promise.resolve/Promsise.reject是两个最常用的静态方法,它们的逻辑也很简单:
- Promise.resolve(x):
- 判断x是不是promise,如果是的话,直接返回。
- 如果x不是promise,创建一个新的promise,并以x为参数resolve该promise。
- Promise.reject(r):
- 创建一个新的promise,并以r为参数reject该promise。
class Promise {
// ...
static resolve(x){
if (x instanceof Promise) return x
return new Promise(resolve => resolve(x))
}
static reject(r){
return new Promise((_, reject) => reject(r))
}
}
而对于Promise.all / Promise.any / Promise.allSettled / Promise.race这些静态方法,它们的大体逻辑是一样的,只是终止条件不同。它们都接受一个可迭代对象(iterable)为参数,并做以下事情:
- 创建
new_promise。 - 遍历iterable,对于遍历过程中的每一个值
i,分别执行Promise.resolve(i),最终得到一个promises列表。 - 对
promises列表中的每个元素p,分别使用then方法注册handler:p.then(onFulfilled, onRejected)。而这四种静态方法的差异,就在于onFulfilled与onRejected的不同:-
Promise.all:
onFulfilled:如果此时其他所有的p状态都为fulfilled,resolvenew_promise。onRejected:直接rejectnew_promise。
-
Promise.any:
onFulfilled:直接resolvenew_promise。onRejected:如果此时其他所有的p状态都为rejected,rejectnew_promise。
-
Promise.allSettled:
onFulfilled:如果此时其他所有的p状态都不是pending,resolvenew_promise。onRejected:如果此时其他所有的p状态都不是pending,resolvenew_promise。
-
Promise.race:
onFulfilled:直接resolvenew_promise。onRejected:直接rejectnew_promise。
-
如果你小学数学掌握得还不错,你会发现把这4个静态方法放在一起,呈现出一种几何上的美感,它们就好像对称地坐落在坐标系上4个不同的象限一样。
以下,我列出了Promise.all、Promise.allSettled两个静态方法的代码示例:
class Promise {
// ...
static all(iterable) {
return new Promise((resolve, reject) => {
let promises = [...iterable].map(i => Promise.resolve(i))
let values = [...iterable].map(_ => undefined)
let remainingElementsCount = 0 // 计算未fulfilled的p
promises.forEach((p, index) => {
remainingElementsCount++
p.then(
(x) => { // onFulfilled
remainingElementsCount--
values[index] = x
if (remainingElementsCount === 0) {
// 此时其他所有的p状态都为fulfilled
resolve(values)
}
},
(reason) => reject(reason)) // onRejected
})
})
}
static allSettled(iterable) {
return new Promise((resolve) => {
let promises = [...iterable].map(i => Promise.resolve(i))
let values = [...iterable].map(_ => undefined)
let remainingElementsCount = 0 // 计算未settled的p
promises.forEach((p, index) => {
remainingElementsCount++
p.then(
(x) => { // onFulfilled
remainingElementsCount--
values[index] = {
status: "fulfilled",
value: x
}
if (remainingElementsCount === 0) {
// 此时其他所有的p状态都不是pending
resolve(values)
}
},
(reason) => { // onRejected
remainingElementsCount--
values[index] = {
status: "rejected",
reason
}
if (remainingElementsCount === 0) {
// 此时其他所有的p状态都不是pending
resolve(values)
}
})
})
})
}
}
至此,一个简易的Promise就完成了。
Async/await的异步实现
在16.生成器中,我已经为你展示了generator是如何实现异步编程的,其关键就是对执行上下文的保存与恢复。实际上,Async函数对异步的实现方式,与generator本质上是一样的,以下是它们之间的算法对比:
| Async函数 | generator | |
|---|---|---|
| 创建抽象闭包、初始化执行上下文 | AsyncFunctionStart | GeneratorStart |
| 暂停执行 | Await | GeneratorYield |
| 恢复执行 | 由微任务队列执行时恢复 | GeneratorResume |
你也可以通过以下代码看到Async函数相关抽象操作的调用顺序:
async function a(){
// 1. 触发【AsyncFunctionStart】
console.log(1)
await 0; // 2. 触发【Await】
console.log(2); // 3. 微任务队列执行时恢复执行
await 0; // 4. 触发【Await】
console.log(3); // 5. 微任务队列执行时恢复执行
}
a()
从执行Async函数体的EvaluateAsyncFunctionBody运行时语义我们可以看出,Async函数会先创建一个promise,然后启动AsyncFunctionStart的执行,执行完毕之后,会返回这个promise。
AsyncFunctionStart的逻辑如下:
这里的关键逻辑是:
- AsyncFunctionStart:
- (1)使变量
runningContext为当前执行上下文(由于此时正在执行Async函数,所以runningContext是由Async函数创建的,图中使用红色标注)。 - (2)使变量
asyncContext为runningContext的一份拷贝。(图中使用黄色标注) - (4)启动AsyncBlockStart:
- (3)创建一个抽象闭包(青色的
closure),这个抽象闭包封装了一段逻辑:执行Async函数体内的语句列表(绿色的asyncBody),并根据执行的结果,选择resolve或者reject由Async函数创建的promise。 - (4)设置
asyncContext的code evaluation state组件,使得每次asyncContext压入调用栈栈顶并恢复执行时,抽象闭包从原来暂停的地方继续执行。 - (5、6)把
asyncContext压入调用栈栈顶,并启动抽象闭包的执行
- (3)创建一个抽象闭包(青色的
- (1)使变量
Generator函数的执行会创建generator但不会立即执行这个过程中创建抽象闭包;而Async函数的执行不仅会创建promise,还会直接启动抽象闭包的执行。
在执行抽象闭包的过程中,当遇到await语句的时候,会触发抽象操作Await:
这里的关键逻辑如下:
- (2)对await表达式后面的值(青色的
value)使用Promise.resolve(value),得到一个新的promise(绿色的promise)。 - (3)创建一个抽象闭包(棕色的
fulfilledClosure),这个抽象闭包会在后续作为promisethen方法的第一个参数使用,它会把asyncContext重新压入调用栈栈顶,并以传入的参数(红色的v)恢复其执行。 - (5)再创建一个抽象闭包(蓝色的
rejectedClosure),这个抽象闭包会在后续作为promisethen方法的第二个参数使用,它会把asyncContext重新压入调用栈栈顶,并以传入的参数(粉红色的reason)恢复其执行。 - (7)通过
promsise.then()把两个抽象闭包注册到promise上 - (8)移除
asyncContext,恢复原来asyncContext下面的执行上下文的执行。
整个await的逻辑,相当于以下的代码:
const promise = Promise.resolve(value)
const fulfilledClosure = (v) => {/*以参数v恢复执行*/}
const rejectedClousure = (reason) => {/*以参数reason恢复执行*/}
promise.then(fulfilledClosure, rejectedClousure)
// 移除asyncContext
这就解释了为什么当await表达式所创建的promise被resolve的时候,Async函数内的逻辑会重新启动执行。由此,Async函数就实现了异步编程。
在Async函数中首次遇到await的时候,此时调用栈中,asyncContext下方的执行上下文是由Async函数创建的执行上下文runningContext。因此,当asyncContext弹出后,会执行EvaluateAsyncFunctionBody运行时语义中未完成的逻辑,即返回一个promise,如以下代码所示:
async function a(){
console.log(1)
await 0;
console.log(2);
await 0;
console.log(3);
}
console.log("Promise: " + a())
// 1
// Promise: [object Promise]
// 2
// 3
返回promise之后,runningContext就被销毁了,之后每一次恢复执行,使用的都是asyncContext。
在8.执行环境,我们早已对Async/await执行过程中调用栈的变化进行了可视化,现在你可以再看一次这张图,相信你会有许多新的感悟:
async function a(){
console.log(1)
await 0;
console.log(2);
await 0;
console.log(3);
}
a()
这段代码的执行过程中,调用栈的变化如下图所示:(出于易读性的考虑,此图忽略了上面提到的runningContext)