灏天阁

await 中那些不为人知的细节

· Yin灏

前言

前置知识:
其实你不知道 Promise.then
你真的明白 promise 的 then 吗?

本文为此专栏的第三篇的文章,在之前的文章中我们剖析 v8 引擎 中实现 promise.then 的处处细节,在之前的文章中我们提及了很多 重要的概念 以及 异步模型,为了你拥有更好的阅读体验,极其建议你先看完本专栏的前两篇文章

1. async 函数的返回值

在讨论 await 之前, async 函数是不可避免的话题不可避免

关于 async 函数处理返回值,也像 Promise.prototype.then 一样会对返回值的类型进行辨识:根据返回值的类型,引起 js 引擎 对返回值处理方式的不同

结论:async 函数在抛出返回值会根据返回值类型开启不同数目的 微任务

  1. return 非  thenable  接口:不落后
  2. return thenable  接口的非  promise,落后  1 个 then  的时间
  3. return promise,落后  2 个 then  的时间

在将返回值抛出之前仍然会进行 Promise.resolve 包装

就像下面的这三个例子一样

(async function getNumber() {
  return 1;
})().then(() => console.log(1));

Promise.resolve()
  .then(() => console.log(2))
  .then(() => console.log(3));

// 1 2 3
(async function getNumber() {
  return {
    then(cb) {
      cb();
    },
  };
})().then(() => console.log(1));

Promise.resolve()
  .then(() => console.log(2))
  .then(() => console.log(3));

// 2 1 3
(async function getNumber() {
  return Promise.resolve();
})().then(() => console.log(1));

Promise.resolve()
  .then(() => console.log(2))
  .then(() => console.log(3));

// 2 3 1

如果你对这些输出顺序仍然感到困惑,再次建议你回顾本专栏的前两篇文章:
其实你不知道 Promise.then
你真的明白 promise 的 then 吗?

async 关键字只是一个函数标识符,在不使用 await 情况下,只能通过 控制返回值 从而产生异步的 微任务

2. await 与异步

2.1 await 只是暂停函数执行

await 关键字的作用和 yield 关键字一模一样,都具有控制函数内部的 暂停恢复执行 的能力,如果你不了解 yield生成器,你可以阅读 《你不知道的生成器 - 掘金 (juejin.cn)》 这篇文章。

async function test() {
  console.log(1);
  await 1;
  console.log(2);
}

test();
console.log(3);

// 1 3 2

执行顺序:
(1)执行 test() 函数,同步执行 函数体中的代码,输出 1

(2)遇到 await 关键字停止执行,因为 await 右边是一个可用的值,所以立即向 微任务队列 添加一个 微任务,这个任务会 恢复异步函数的执行

(3)输出3,同步线程代码执行完毕

(4)js 运行时从微任务队列取出任务,恢复函数执行,await 去得值 1,这时候 await 1 的值就是 1

(5)执行函数剩余部分,输出 2

其实在这个过程中,我们更应该关心的是 await 求值的过程,就像下面的代码表现出来的一样

function getNumber() {
  console.log(2);
}

async function test() {
  console.log(1);
  await getNumber();
  console.log(3);
}

test();
console.log(4);

// 1 2 4 3

getNumber() 函数先执行,然后再暂停函数,等待同步任务结束后,从微任务队列中取出恢复函数执行的任务并且执行后再将 await 中右边的值赋值给 await

2.2 await 求值顺序

如果 await 右边的函数有异步执行的任务,又是什么情况呢

function getNumber() {
  console.log(2);
  Promise.resolve()
    .then(() => console.log(5))
    .then(() => console.log(6))
    .then(() => console.log(7));
}

async function test() {
  console.log(1);
  await getNumber();
  console.log(3);
}

test();
console.log(4);

// 1 2 4 5 3 6 7

你会发现,await 之后的内容并非会在 getNumber 执行完成之前进行

结论:在时间顺序上 await 之后的内容只会 等待一个 右值内部 then 的时间,并非等待 右边的值 全部执行完

2.3 await 右值类型

如果 await 右值是一个 thenable 或者 promise 呢,又是什么情况呢

async function test() {
  console.log(1);
  await {
    then(cb) {
      cb();
    },
  };
  console.log(2);
}

test();

Promise.resolve()
  .then(() => console.log(3))
  .then(() => console.log(4))
  .then(() => console.log(5))
  .then(() => console.log(6));

// 1 3 2 4 5 6

如果 await 右边是个 thenableawait 后面的内容只会等待一个 then 的时间

async function test() {
  console.log(1);
  await Promise.resolve();
  console.log(2);
}

test();

Promise.resolve()
  .then(() => console.log(3))
  .then(() => console.log(4))
  .then(() => console.log(5))
  .then(() => console.log(6));

// 1 2 3 4 5 6

如果 await 右边是个 promiseawait 后面的内容只会为什么表现的和非 thenable 值一样呢?为什么不等待两个 then 的时间呢?

TC 39 对 await 后面是 promise 的情况如何处理进行一次修改,在早期版本,依然会等待两个 then 的时间

掘金上有大佬翻译了官方解释:《「译」更快的 async 函数和 promises - 掘金 (juejin.cn)》,但在这次更新中并没有修改 thenable 的情况

这样做的好处,极大优化了 await 等待的速度

async function getNumber() {
  console.log(0);
  await 1;
  console.log(7);
  await 2;
  console.log(8);
  await 3;
  console.log(9);
}

async function test() {
  console.log(1);
  await getNumber();
  console.log(2);
}

test();

Promise.resolve()
  .then(() => console.log(3))
  .then(() => console.log(4))
  .then(() => console.log(5))
  .then(() => console.log(6));

// 1 0 7 3 8 4 9 5 2 6

所以你能合并两个函数的代码:

async function test() {
  console.log(1);
  console.log(0);
  await 1;
  console.log(7);
  await 2;
  console.log(8);
  await 3;
  console.log(9);
  await null;
  console.log(2);
}

test();

Promise.resolve()
  .then(() => console.log(3))
  .then(() => console.log(4))
  .then(() => console.log(5))
  .then(() => console.log(6));

// 1 0 7 3 8 4 9 5 2 6

又完全可以等效如下代码:

async function test() {
  console.log(1);
  console.log(0);
  Promise.resolve()
    .then(() => console.log(7))
    .then(() => console.log(8))
    .then(() => console.log(9))
    .then(() => console.log(2));
}

test();

Promise.resolve()
  .then(() => console.log(3))
  .then(() => console.log(4))
  .then(() => console.log(5))
  .then(() => console.log(6));

// 1 0 7 3 8 4 9 5 2 6

这三种写法在时间的顺序上完全等效,所以你 完全可以将 await 后面的代码可以看做在 then 里面执行的结果,又因为 async 函数会返回 promise 实例,所以你还可以等效成:

async function test() {
  console.log(1);
  console.log(0);
}

test()
  .then(() => console.log(7))
  .then(() => console.log(8))
  .then(() => console.log(9))
  .then(() => console.log(2));

Promise.resolve()
  .then(() => console.log(3))
  .then(() => console.log(4))
  .then(() => console.log(5))
  .then(() => console.log(6));

// 1 0 7 3 8 4 9 5 2 6

这样你会发现,async test 函数内部全是同步的代码,对于你观察 promise 执行顺序是不是更清晰呢。

为什么能做这种等效时间替换呢?

await 会暂停 async 函数的执行,等到 js 运行时 取出恢复函数执行的微任务并执行,在这个过程中仅仅发生了一个 then 的时间

3. 总结

  1. async 函数会根据返回值类型的不同产生 0 - 2 个的 微任务,然后对返回值进行 Promise.resolve 包装并抛出
  2. await 关键字的作用:暂停函数的执行,暂停时间等效为 一个 then 的时间,在这之后立即将右值结果赋值给 await,并非等到等到右值完全执行完毕,才会执行后面的内容

结语

到此,如果你完整阅读过 《从 v8 看 promise》专栏中的 三篇文章,相信你足以面对绝大数关于 promise 的面试题,如果再掌握了一点 宏任务 的知识,相信没有你理解不了 promise 执行顺序的问题了。

- Book Lists -