协程
协程是一种比线程更加轻量级的存在。可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程,比如当前执行的是 A 协程,要启动 B 协程,那么 A 协程就需要将主线程的控制权交给 B 协程,这就体现在 A 协程暂停执行,B 协程恢复执行;同样,也可以从 B 协程中启动 A 协程。通常,如果从 A 协程启动 B 协程,我们就把 A 协程称为 B 协程的父协程。
协程有点像函数,又有点像线程。它的运行流程大致如下。
第一步,协程A开始执行。
第二步,协程A执行到一半,进入暂停,执行权转移到协程B。
第三步,(一段时间后)协程B交还执行权。
第四步,协程A恢复执行。
上面流程的协程A,就是异步任务,因为它分成两段(或多段)执行。
举例来说,读取文件的协程写法如下。
function asnycJob() { // ...其他代码 var f = yield readFile(fileA); // ...其他代码 }
上面代码的函数 asyncJob 是一个协程,在执行到其中的 yield 命令处时,执行权将交给其他协程。也就是说,yield命令是异步两个阶段的分界线。
协程遇到 yield 命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。它的最大优点,就是代码的写法非常像同步操作,如果去除yield命令,简直一模一样。
Generator
Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。
// generator 函数
function* foo() {
let response1 = yield fetch('http://back.avatar.movie.test.sankuai.com/')
console.log('response1')
console.log(response1)
let response2 = yield fetch('http://back.avatar.movie.test.sankuai.com/')
console.log('response2')
console.log(response2)
}
// 执行 foo 函数的代码
let gen = foo()
function getGenPromise(gen) {
return gen.next().value
}
getGenPromise(gen).then((response) => {
console.log('response1')
console.log(response)
return getGenPromise(gen)
}).then((response) => {
console.log('response2')
console.log(response)
})
- 首先执行的是
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 框架),如下面这种方式:
function* foo() {
let response1 = yield fetch('https://www.geekbang.org')
console.log('response1')
console.log(response1)
let response2 = yield fetch('https://www.geekbang.org/test')
console.log('response2')
console.log(response2)
}
co(foo());
通过使用生成器配合执行器,就能实现使用同步的方式写出异步代码了,这样也大大加强了代码的可读性。
co 源码分析
Co 核心代码(删去了非核心代码)
function co(gen) {
if (typeof gen === 'function') gen = gen.apply(ctx, args);
onFulfilled();
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}
function next(ret) {
if (ret.done) return resolve(ret.value);
var value = toPromise.call(ctx, ret.value);
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
}
这儿,在给co传入一个generator
函数后,co会将其自动启动。然后调用onFulfilled
函数。在onFulfilled
函数内部,首先则是获取next的返回值。交由next
函数处理。 而next
函数则首先判断是否完成,如果这个generator函数完成了,返回最终的值。否则则将yield
后的值,转换为Promise
。最后,通过Promise
的then,并将onFulfilled
函数作为参数传入。
if (value && isPromise(value)) {
return value.then(onFulfilled, onRejected);
}
而在generator
中,yield
句本身没有返回值,或者说总是返回undefined
。 而next方法可以带一个参数,该参数就会被当作上一个yield
语句的返回值。同时通过onFulfilled
函数,则可以实现自动调用。这也就能解释为什么co基于Promise
。且能自动执行了。
Async/await
async 到底是什么?根据 MDN 定义,async 是一个通过异步执行并隐式返回 Promise 作为结果的函数。
对 async 函数的理解,需要重点关注两个词:异步执行和隐式返回 Promise。
async function foo() {
return 2
}
// async 函数返回一个 promise
console.log(foo()) // Promise {<resolved>: 2}
await 到底是什么?
async function foo() {
console.log(1)
let a = await 100
console.log(a)
console.log(2)
}
console.log(0)
foo()
console.log(3)
根据上面的代码来分析 async/await 的执行流程。
首先,执行console.log(0)
这个语句,打印出来 0。
- 执行
console.log(0)
- 执行 foo 函数,foo 是 async 函数,js 引擎保留当前的调用栈等信息
- 执行 foo 函数中
console.log(1)
- 执行
await 100
,当遇到await 100
语句,js 引擎默认创建一个 promise 对象,大致代码如下:
let promise_ = new Promise((resolve,reject){
resolve(100)
})
- js 引擎执行到
resolve(10)
将任务提交给微任务队列 - 然后 JavaScript 引擎会暂停当前协程的执行,将主线程的控制权转交给父协程执行,同时会将 promise_ 对象返回给父协程
- 父协程拿到主线程控制权,做的一件事是调用 promise_.then 来监控 promise 状态的改变
- 继续执行
console.log(3)
- 父协程将执行结束,在结束之前,检查微任务队列,然后执行微任务队列
- 执行
resolve(100)
,触发 promise_.then 中的回调函数:
promise_.then((value)=>{
// 回调函数被激活后
// 将主线程控制权交给 foo 协程,并将 vaule 值传给协程
})
- 将主线程的控制权交给 foo 函数的协程,并同时将 value 值传给该协程
- foo 协程激活之后,会把刚才的 value 值赋给了变量 a
- 执行 foo 函数的后续语句,执行完成之后,将控制权归还给父协程。
- 完毕。
以上就是 await/async 的执行流程。正是因为 async 和 await 在背后为我们做了大量的工作,所以我们才能用同步的方式写出异步代码来。
Co 源码详细分析
/**
* 执行 generator function 或者 generator,
* 返回一个 Promise
* @param {Function} fn
* @return {Promise}
* @api public
*/
function co(gen) {
var ctx = this;
var args = slice.call(arguments, 1)
// we wrap everything in a promise to avoid promise chaining,
// 把传进来的所有东西都转为 promise
// which leads to memory leak errors.
// see https://github.com/tj/co/issues/180
return new Promise(function(resolve, reject) {
// 执行 gen 函数
if (typeof gen === 'function') gen = gen.apply(ctx, args);
// gen函数返回的 gen 指针不存在或 gen.next 不是函数(意味着 gen 不是 Generator函数) 则返回空值
if (!gen || typeof gen.next !== 'function') return resolve(gen);
onFulfilled();
/**
* @param {Mixed} res
* @return {Promise}
* @api private
* 把每次 yield 之后的异步函数的返回结果当做参数传回 generator 函数
* eg. let a = yeild b
* 此时经过 onFulfilled 函数的处理 a === b()
*/
function onFulfilled(res) {
var ret;
try {
/**
* gen.next(res),则是向generator函数传参数,作为yield的返回值
* ret 是 gen.next() 返回的对象
* ---
* yield 语句本身没有返回值,或者说总是返回undefined。
* next方法可以带一个参数,该参数就会被当作上一个yield语句的返回值。
*/
ret = gen.next(res);
} catch (e) {
return reject(e);
}
// 每完成一次 yield,把返回的对象交给 next() 处理
next(ret);
}
/**
* @param {Error} err
* @return {Promise}
* @api private
*/
function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}
/**
* Get the next value in the generator,
* return a promise.
* 把 gen.next() 返回对象中的 value 变为 promise
*
* @param {Object} ret
* @return {Promise}
* @api private
*/
function next(ret) {
/**
* 如果这个generator函数完成了,返回最终的值
* 假设我们写的 generator函数没有 return someValue, 在所有yield完成后,调用next()会返回{valu: undefined, done: true}
* 所以需要手动return一个值。这样最后的value才不是undefined
*/
if (ret.done) return resolve(ret.value);
// 这个generator函数还没结束, 就统一交给 toPromise() 处理
var value = toPromise.call(ctx, ret.value);
// 这里value.then(onFulfilled, onRejected),实际上已经调用并传入了 onFulfilled, onRejected 两个参数。
// 把 onFulfilled 函数传入 value
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
});
}
剩下的(thunkToPromise, arrayToPromise)这些辅助函数就不写了