之前我在关于Promise的文章中提到了co
这个库。在这篇文章里,我将写一写自己对它的认识。
Trust me,用了co
库,你不想用别的,来它半斤异步调用你一口能吃仨。
但是我对Tj大神的co库源码谈不上深入理解。所以,如有乱讲,欢迎指正。
我这里默认读者对Promise
和Generator
有一定的认识。
先安利自己写的两篇关于Promise的文章:
下面我就来谈谈co
这个牛逼的库。
ES7 async/await
干嘛,我们不是讲ES6么,怎么跳到ES7了?
因为co
要做的事情,就是ES7的async/await
要做的事情。
也就是说,这种解决异步的思路,已经在ECMA标准的考虑之中了。将来我们浏览器的JS引擎就可以原生实现这件事而不是通过JavaScript代码模拟。要知道,引擎的实现和代码的实现那是完全两码事。
一点题外话
多一句嘴:有些同学混淆了ECMA标准、引擎支持和代码实现的联系。
这里引用老赵在知乎里面回答问题时说的一句话:
ES7是个标准,定义的是what to do不是how to do,为什么好多人还是搞不清这两者的区别。
ECMAScript定义了一些JavaScript语言层面要做的事情,这是一个标准。之所以要制定这个标准,是为了防止浏览器各自为政而出现JS引擎对同一行代码的解释出现不同的情况。
也就是说,ECMA制定标准,我们就可以按照这个标准来写JavaScript代码。写好的JavaScript代码由浏览器的JS引擎来解释,最终变成计算机能读懂的代码来执行。
async/await
上代码:
var foo = function(){
return new Promise(resolve => {
// 异步操作之后
resolve('OK');
});
}
async funtion bar(){
var result = await foo();
console.log(result);
}
bar(); // ==> 打印'OK'
我们注意到,这段代码用了两个新的关键字async
和await
。而且有两件神奇的事情发生了:
bar
函数中包含了一个返回Promise
对象的语句,而且Promise
中存在异步代码。但是这条语句接下来的语句明显是等待Promise
对象中的代码异步执行完毕之后才执行的。(否则不会得到异步之后的值)Promise
对象resolve
的值,并没有在then
中进行处理,而是直接作为返回值返回到Promise
对象外面了.
这就是async/await
的魔法。在函数前面加上async
关键字之后,内部的代码会识别await
关键字。此时假设await
后面的语句返回一个Promise
对象,那么执行的代码将会等待,直到Promise
对象变为resolve
状态。并且Promise
对象中resolve
的值将直接作为await
语句的返回值返回。然后再执行await
语句之后的语句。
从此我们就可以无痛的撸异步代码,妈妈再也不用担心回调金字塔的出现和异步流程逻辑搞不定的情况了!
另一个奇妙的事情就是,率先支持这一特性的浏览器居然是微软的Edge。大概是因为C#
语言早就出现async/await
,并且TypeScript
也支持这一特性的缘故吧。
co
我们希望所有的浏览器都及早支持这一特性。但是值得欣喜的一点就是,虽然V8还没有支持,Tj大神早就利用Generator
的方式实现了一个ES6版本的async/await
!(膜拜脸)
co函数形式
同样是上面的逻辑,我们用co
实现一次:
// 首先我们需要将co引入,假设我们使用commonJS的方式
const co = require('co');
var foo = function(){
return new Promise(resolve => {
// 异步操作之后
resolve('OK');
});
}
co(function* (){
var result = yield foo();
console.log(result);
}); // ==> 打印'OK'
我们看到,co
函数接收一个Generator
生成器函数作为参数。执行co
函数的时候,生成器函数内部的逻辑像async
函数调用时一样被执行。不同之处只是这里的await
变成了yield
。
简单版本的co代码
要实现以上的逻辑,结合Generator
的特性,co
函数应该:
在函数体内将Generator生成器函数执行并生成生成器实例(在此命名为
gen
),然后通过gen.next
方法的调用,不断执行生成器函数内部的代码。执行
next
方法之后,返回的Promise
在生成器函数执行环境之外执行,并取出resolve
值,作为返回值作为next
方法的参数返回到Generator
执行环境中。
基于以上两点,我们可以大体实现一个简化版的co
,代码如下:
const co = function(genFunc){
const gen = genFunc(); // 得到生成器实例
const deal = (val) => {
const res = gen.next(val);
// 这里处理了异步逻辑,
// 在回调中去递归,不断执行next
// 这样就将resolve的值传回了Generator
res.value.then(result => deal(result));
}
deal(); // 第一次触发递归
}
去掉括号等等,只有短短六行代码。
more
原理性的东西大约就是这样了。但是co
做的不止这些。
之前
co
的yield
后的语句并不支持Promise
对象,而是一个特殊的函数,叫做thunk
。目前co
二者都支持。
此处我并不打算重复性解释thunk
版本,因为原理性的东西实现起来是差不多的。-
co
函数是有返回值的,也是一个Promise
对象。当生成器函数内的逻辑执行完毕且没有错误之后,这个
Promise
对象(co
返回值)变为resolve
状态,且将生成器的返回值作为resolve
出来的值。若生成器函数内返回一个
Promise
对象,那么co
函数返回值就是这个Promise
对象。若生成器函数抛出了错误,那么这个错误作为
reject
出来的值,将Promise
对象的状态变为reject
。
这样我们就可以将错误放进其返回值的
.catch
方法中统一处理。 在生成器函数内部,我们也可以使用
try...catch
语句获取错误对象。生成器的
yield
后面可以跟一个元素值为Promise
对象的数组,这个数组内Promise
对象内的异步逻辑将并发执行,并返回一个数组。(类似于Promise.all
方法)假设生成器执行之前需要从外部传入参数,
co
库提供了一个方法:
var fn = co.wrap(function* (val) {
return yield Promise.resolve(val);
});
fn(true).then(function (val) {
});
结束
以上是一点微小的见解。谢谢指正。