从 generator 到 async/await
协程
协程是一种比线程更加轻量级的存在。可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程,比如当前执行的是 A 协程,要启动 B 协程,那么 A 协程就需要将主线程的控制权交给 B 协程,这就体现在 A 协程暂停执行,B 协程恢复执行;同样,也可以从 B 协程中启动 A 协程。通常,如果从 A 协程启动 B 协程,我们就把 A 协程称为 B 协程的父协程。
协程有点像函数,又有点像线程。它的运行流程大致如下。
第一步,协程 A 开始执行。
第二步,协程 A 执行到一半,进入暂停,执行权转移到协程 B。
第三步,(一段时间后)协程 B 交还执行权。
第四步,协程 A 恢复执行。
上面流程的协程 A,就是异步任务,因为它分成两段(或多段)执行。
举例来说,读取文件的协程写法如下。
1
2
3
4
5 function asnycJob() {
// ...其他代码
var f = yield readFile(fileA);
// ...其他代码
}
上面代码的函数 asyncJob 是一个协程,在执行到其中的 yield 命令处时,执行权将交给其他协程。也就是说,yield 命令是异步两个阶段的分界线。
协程遇到 yield 命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。它的最大优点,就是代码的写法非常像同步操作,如果去除 yield 命令,简直一模一样。
Generator
Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。
1 | // generator 函数 |
- 首先执行的是
let gen = foo(),创建了 gen 协程。 - 然后在父协程中通过执行 gen.next 把主线程的控制权交给 gen 协程。
- gen 协程获取到主线程的控制权后,就调用 fetch 函数创建了一个 Promise 对象 response1,然后通过 yield 暂停 gen 协程的执行,并将 response1 返回给父协程。
- 父协程恢复执行后,调用 response1.then 方法等待请求结果。
- 等通过 fetch 发起的请求完成之后,会调用 then 中的回调函数,then 中的回调函数拿到结果之后,通过调用 gen.next 放弃主线程的控制权,将控制权交 gen 协程继续执行下个请求。
通过 Generator 和 Promise 相互配合执行,达到了将异步操作以同步方式书写的目的。不过通常,我们把执行生成器的代码封装成一个函数,并把这个执行生成器代码的函数称为执行器
(可参考著名的 co 框架),如下面这种方式:
1 | function* foo() { |
通过使用生成器配合执行器,就能实现使用同步的方式写出异步代码了,这样也大大加强了代码的可读性。
co 源码分析
Co 核心代码(删去了非核心代码)
1 | function co(gen) { |
这儿,在给 co 传入一个generator函数后,co 会将其自动启动。然后调用onFulfilled函数。在onFulfilled函数内部,首先则是获取 next 的返回值。交由next函数处理。 而next函数则首先判断是否完成,如果这个 generator 函数完成了,返回最终的值。否则则将yield后的值,转换为Promise。最后,通过Promise的 then,并将onFulfilled函数作为参数传入。
1 | if (value && isPromise(value)) { |
而在generator中,yield句本身没有返回值,或者说总是返回undefined。 而 next 方法可以带一个参数,该参数就会被当作上一个yield语句的返回值。同时通过onFulfilled函数,则可以实现自动调用。这也就能解释为什么 co 基于Promise。且能自动执行了。
Async/await
async 到底是什么?根据 MDN 定义,async 是一个通过异步执行并隐式返回 Promise 作为结果的函数。
对 async 函数的理解,需要重点关注两个词:异步执行和隐式返回 Promise。
1 | async function foo() { |
await 到底是什么?
1 | async function foo() { |
根据上面的代码来分析 async/await 的执行流程。
首先,执行console.log(0)这个语句,打印出来 0。
- 执行
console.log(0) - 执行 foo 函数,foo 是 async 函数,js 引擎保留当前的调用栈等信息
- 执行 foo 函数中
console.log(1) - 执行
await 100,当遇到await 100语句,js 引擎默认创建一个 promise 对象,大致代码如下:
1 | let promise_ = new Promise((resolve,reject){ |
- js 引擎执行到
resolve(10)将任务提交给微任务队列 - 然后 JavaScript 引擎会暂停当前协程的执行,将主线程的控制权转交给父协程执行,同时会将 promise_ 对象返回给父协程
- 父协程拿到主线程控制权,做的一件事是调用 promise_.then 来监控 promise 状态的改变
- 继续执行
console.log(3) - 父协程将执行结束,在结束之前,检查微任务队列,然后执行微任务队列
- 执行
resolve(100),触发 promise_.then 中的回调函数:
1 | promise_.then(value => { |
- 将主线程的控制权交给 foo 函数的协程,并同时将 value 值传给该协程
- foo 协程激活之后,会把刚才的 value 值赋给了变量 a
- 执行 foo 函数的后续语句,执行完成之后,将控制权归还给父协程。
- 完毕。
以上就是 await/async 的执行流程。正是因为 async 和 await 在背后为我们做了大量的工作,所以我们才能用同步的方式写出异步代码来。
Co 源码详细分析
1 | /** |
剩下的(thunkToPromise, arrayToPromise)这些辅助函数就不写了
从 generator 到 async/await
https://downhill6.pages.dev/2019/12/23/ 从 generator 到 async_await/