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),按以下规范实现:

  1. asyncContext 作为当前的执行上下文
  2. promise = PromiseResolve(%Promise%, value).
  3. 定义 stepsFulfilled,它的执行步骤定义在 Await Fulfilled Functions.
  4. onFulfilled = CreateBuiltinFunction(stepsFulfilled, « [[AsyncContext]] »).
  5. onFulfilled.[[AsyncContext]] = asyncContext.
  6. 定义 stepsRejected,它的执行步骤定义在Await Rejected Functions.
  7. onRejected = CreateBuiltinFunction(stepsRejected, « [[AsyncContext]] »).
  8. onRejected.[[AsyncContext]] = asyncContext.
  9. 执行 PerformPromiseThen(promise, onFulfilled, onRejected).
  10. 将 asyncContext 从执行上下文栈顶移除
  11. 设置 asyncContext 代码的运行状态,以便在未来恢复运行后,await 之后的代码能够被完全执行。
  12. 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 的感觉,你可以试试,然后麻烦回来告诉我原因,感谢~!

posted @ 2019-12-27 10:33  Liaofy  阅读(557)  评论(0编辑  收藏  举报