什么是生成器函数?

生成器函数是一个带星号的函数,可以暂停执行与恢复执行。
async/await 使用了 协程(Generator) 和 微任务(Promise) 两种技术来实现。

  1. function* genDemo() {
  2. console.log("开始执行第 1 段");
  3. yield "generator 1";
  4. console.log("开始执行第 2 段");
  5. yield "generator 2";
  6. console.log("执行结束");
  7. return "generator 3";
  8. }
  9. console.log("main 0");
  10. let gen = genDemo(); // 此处不会打印 func 1 只有在执行 gen.next 时才会执行
  11. console.log(gen.next().value); // func1 generator 1
  12. console.log("main 1");
  13. console.log(gen.next().value);
  14. console.log("main 2");
  15. console.log(gen.next().value);
  16. console.log("main 3");
  • 输出内容如下
    main 0
    开始执行第 1 段
    generator 1
    main 1
    开始执行第 2 段
    generator 2
    main 2
    执行结束
    generator 3
    main 3

从上面输出结果可以看出,生成器函数与主函数是交替执行的。
生成器函数中遇到 yield 关键字时,就会返回 yield 后的内容给外部并把执行权交给外部函数去执行。
外部函数又可以通过 gen.next 恢复生成器函数的执行。

什么是协程?

协程是一种比线程更加轻量级的存在,可以看成是跑在线程上的任务。就像一个进程可以有多个线程一样,一个线程也可以有多个协程。
但是,线程上同时只能执行一个协程。比如:当前执行的是 A 协程,要启动 B,就需要将主线程的控制权交给 B 协程;A 暂停执行,B 恢复执行。通常,如果从 A 协程启动 B 协程,我们就把 A 协程称为 B 协程的父协程。
协程执行流程图.png
从图中可以看出:

  1. 通过生成器函数 genDemo 创建的协程 gen 创建之后并没有立即执行。
  2. 通过调用 gen.next 可以使协程执行。
  3. 通过 yield 关键字来暂停 gen 协程的执行,并返回主要信息给父协程。
  4. 若在执行期间遇到 return,JS 引擎会结束当前协程并将 return 后的内容返回给父协程。
  • 父协程与 gen 协程都有自己的调用栈,当控制权通过 yield 与 gen.next 互相切换时,V8 是如何切换调用栈的?
  1. gen 协程与父协程是在主线程上交互执行的,并不是并发执行的,它们之间的切换是通过 yield 与 gen.next 配合完成。
  2. gen 中调用 yield 时,JS 引擎会保存 gen 协程当前的调用栈信息并恢复父协程的调用栈信息。同理,父协程中执行 gen.next 时,JS 引擎会保存父协程调用栈信息并恢复 gen 协程的调用栈信息。如下图:

协程间的切换.png

async/await

async 是什么?

async 是一个通过异步执行隐式返回 Promise作为结果的函数。

  • 隐式返回 Promise
  1. async function foo() {
  2. return 2;
  3. }
  4. foo(); // Promise {<resolved>: 2}

await 是什么?

观察下面代码的输出:

  1. async function foo() {
  2. console.log(1);
  3. let a = await 100;
  4. console.log(a);
  5. console.log(2);
  6. }
  7. console.log(0);
  8. foo();
  9. console.log(3);

输出:0 1 3 100 2
执行流程图如下:
async、await执行流程图.png
当执行到 await 100 时,会创建一个 Promise 对象,如下:

  1. let promise_ = new Promise((resolve, reject) => {
  2. resolve(100);
  3. });

JS 引擎会将该任务提交到微任务队列,然后暂停当前协程的执行,将主线程的控制权转交给父协程执行,同时将 promise_ 对象返回给父协程(如下)。

  1. async function foo() {
  2. ...
  3. let a = await 100
  4. ...
  5. }
  6. console.log(foo())
  7. //Promise {<pending>}__proto__: ... "
  8. // [[PromiseStatus]]: "resolved"
  9. // [[PromiseValue]]: undefined

主线程控制权交给父协程后,父协程调用 promise_.then 来监控 promise 状态的改变。

接下来执行父协程的流程,打印出 3。随后父协程将执行结束,在结束前,进入微任务的检查点去执行微任务队列,微任务队列中有 resolve(100) 等待执行,执行到这里时,会触发 promise_.then 中的回调函数,如下:

  1. promise_.then(value => {
  2. // 回调函数触发后,将主线程的控制权交给 foo 协程,并将 value 传给协程
  3. });

foo 协程激活后,将 value 的值给了变量 a,然后继续执行后面语句,执行完成,将控制权归还给父协程。

思考题

  1. async function foo() {
  2. console.log("foo");
  3. }
  4. async function bar() {
  5. console.log("bar start");
  6. await foo();
  7. console.log("bar end");
  8. }
  9. console.log("script start");
  10. setTimeout(() => {
  11. console.log("setTimeout");
  12. }, 0);
  13. bar();
  14. new Promise(resolve => {
  15. console.log("promise executor");
  16. resolve();
  17. }).then(() => {
  18. console.log("promise then");
  19. });
  20. console.log("script end");

输出如下:
scritp start
bar start
foo
promise executor
script end
bar end
promise then
setTimeout

注意点:
第三步会输出 foo,而不是 promise executor.
因为 await 是将 return 的值用 resolve 包装提交到微任务队列,console.log 语句不受影响,可以直接输出。

setTimeout 被放到延迟队列中,而不是下一轮宏任务。
本轮宏任务执行完成后,会执行延迟队列中的任务。
宏任务中父协程执行结束前,会去微任务队列检查执行微任务。