JavaScript await 与 promise 的纠葛
大概在去年的这个时候,V8 团队发布了一篇博文Faster async functions and promises,向我们介绍了他们是如何提升 await 的执行速度,值得一看,这里还有中文版。没有这个前提,看我的这篇文章可能就没啥意义了。
博文中提到了在不同 Node 版本中,await 和 promise 执行时机不同的问题,如下:
可以说,这种“正确的行为”其实并不直观,对 JavaScript 开发者来说实际上是令人惊讶的,所以值得做一些解释。
博文由浅入深讲解了 await 的底层原理和优化过程,说实话,看一遍可能很懵逼。譬如,试着解答下面代码【PP】的执行顺序,以及为什么会这样?
const p = Promise.resolve(42)
const asyncFn = (async function () {
await p;
console.log('after:await', 1);
await p;
console.log('after:await', 2);
})();
asyncFn.then(() => console.log('after:asyncFn'))
Promise.resolve(p).then(() => console.log('tick:1'))
.then(() => console.log('tick:2'))
.then(() => console.log('tick:3'))
.then(() => console.log('tick:4'))
.then(() => console.log('tick:5'))
.then(() => console.log('tick:6'))
.then(() => console.log('tick:7'))
.then(() => console.log('tick:8'))
.then(() => console.log('tick:9'))
所以,今天我尝试结合 V8 团队的文章和 ecma262 await 规范,看看能否直白的说清楚这个过程。Node 版本为 v10.16.3。
规范 6.2.3.1Await
对于 await (value)
,按以下规范实现:
- 把
asyncContext
作为当前的执行上下文 - promise = PromiseResolve(%Promise%, value).
- 定义 stepsFulfilled,它的执行步骤定义在 Await Fulfilled Functions.
- onFulfilled = CreateBuiltinFunction(stepsFulfilled, « [[AsyncContext]] »).
- onFulfilled.[[AsyncContext]] = asyncContext.
- 定义 stepsRejected,它的执行步骤定义在Await Rejected Functions.
- onRejected = CreateBuiltinFunction(stepsRejected, « [[AsyncContext]] »).
- onRejected.[[AsyncContext]] = asyncContext.
- 执行 PerformPromiseThen(promise, onFulfilled, onRejected).
- 将 asyncContext 从执行上下文栈顶移除
- 设置 asyncContext 代码的运行状态,以便在未来恢复运行后,await 之后的代码能够被完全执行。
- return 一个 promise
await 的 V8 表示
假设我们有这样的代码:
async function foo(v) {
const w = await v;
return w;
}
V8 将其翻译成这样:
resumable function foo(v) {
implicit_promise = createPromise();
// 将 v 包装成一个 promise
vPromise = promiseResolve(v)
// 为恢复 foo 函数添加处理器
throwaway = createPromise();
performPromiseThen(vPromise,
res => resume(<<foo>>, res),
err => throw(<<foo>>, err),
throwaway);
// 中断 foo,返回一个 implicit_promise
w = suspend(<<foo>>, implicit_promise);
return resolvePromise(implicit_promise, w);
}
function promiseResolve(v) {
if (v is Promise) return v;
promise = createPromise();
resolvePromise(promise, v);
return promise;
}
performPromiseThen
以上代码的关键,就是 performPromiseThen,在本例中,可以粗浅的理解为:执行 performPromiseThen,会在 microtask 中入队一个PromiseReactionJob,这个 job 做两件事——对将要到来的值运用合适的 handler,然后用 handler 的返回值去 resolve/reject 与该 handler 相关的派生 promise。
看起来有点迷!
其实在本例中,handler 对应 res => resume(<<foo>>, res)
,派生的 promise 对应throwaway
,介此,我们可以用下面的伪代码表示这一行为:
Promise.resolve(value)
.then(res => resume(<<foo>>, res))
.then(res => throwaway(res))
好了,不妨用楼上的代码【PP】,动动手,试着模拟下 microtask 队列中的 job 变化。
代码第一次执行完毕,microtask = [PromiseReactionJob,tick:1],此时无任何输出;
执行 PromiseReactionJob,microtask = [tick:1,resume],此时无输出;
执行 tick:1,microtask = [resume,tick:2],打印tick:1
;
执行 resume,microtask = [tick:2,throwaway],此时无输出;
执行 tick:2,microtask = [throwaway,tick:3],打印tick:2
;
执行 throwaway,microtask = [tick:3,PromiseReactionJob],打印'after:await', 1
;
....
不断地重复以上操作,直到 microtask 为空。
所以,最后的打印结果为:
tick:1
tick:2
after:await 1
tick:3
tick:4
tick:5
after:await 2
tick:6
after:asyncFn
tick:7
tick:8
tick:9
最后的思考
了解了 performPromiseThen 的执行过程,你或许会有一个疑问:throwaway 这个内部 promise 好像没啥用?对,V8 团队也是这么想的,他们向 ecma262 提了一个 issue Spec factoring: allow PerformPromiseThen with no capability。
另外,在代码片段【PP】中,如果把const p = Promise.resolve(42)
换成const p = 42
,会有一种 holy shit 的感觉,你可以试试,然后麻烦回来告诉我原因,感谢~!