最近看了阮一峰老师ES6 Generator 函数的异步应用,一开始没理解为什么要用Generator去解决异步嵌套的问题。网上搜索结果也都比较生硬,不太好理解。所以写了下面这篇文章,帮自己理一下思路。因为是对阮老师文章的解读,所以文章中大量引用阮老师文章和代码,在此一起谢过阮老师。
首先需要你了解Generator函数。
我们先看下阮老师的文章,协程,协程的-Generator-函数实现。这里应该主要是考虑嵌套异步的情况。如果只有一个异步,callback或者promise还是很清晰的。多个回调函数嵌套的时候callback和promise会有这些缺点。
Ps:自己觉得callback 回调参数抽出独立的函数就没有上述问题了,还是很清晰,很好理解的。
嵌套异步就要控制上一次异步执行完成之后,再执行下一个异步。Generator函数可以控制什么时候执行next,如果yield 后面的表达式是异步代码,那就可以控制每一个异步执行。
假如我们能拿到每一步的结果,并将它传递给下一个yield。循环这个过程,就能实现嵌套的异步。需要解决的两个问题,第一拿到yield后面表达式的异步结果,第二异步结果传递给下一个yield表达式。
第一个问题 拿到yield后面表达式的异步结果
next函数返回值是{value:xx,done:boolean}结构,
其中value属性是yield后面表达式的结果。例如下面代码,hw.next()返回结果的value就是'hello'
function* helloWorldGenerator() {
yield 'hello';//'hello'表达式的结果就是'hello'。如果写成这样就好理解点 yield return 'hello';当然这样是有语法错误的。
return 'ending';
}
var hw = helloWorldGenerator();
hw.next()
// { value: 'hello', done: false }
如果表达式是一个异步操作,返回值肯定不是异步结果,因为这里yield直接交出执行权了,不会等异步返回结果。那么只能通过表达式的返回值拿到异步结果。那么这个返回值肯定不是基本类型,Number、Boolean、String、NULL、Undefined,Symbol这些。引用类型Array,Date都是存储数据的,Object范围又太大,一切皆Object。Function比较合适,如果yield后面的表达式返回的是一个函数,如果我们调用这个函数能得到异步结果就可以了。
第二个问题 异步结果传递给下一个yield 表达式
可以通过next函数的参数实现。注意next参数并不是传递给yield后面的表达式的,而是传递给yield左边的变量的。
具体怎么让一个调用异步方法的表达式返回一个函数呢?答案就是用Thunk函数,简单的来说Thunk函数就是将一个有回调参数的函数变形。
将fun(para1,para2,…,callback)变形成funThunk(para1,para2,…)(callback)。funThunk函数返回值是另一个函数,可以接受回调参数。貌似很好的解决了问题,上代码。
var fs = require('fs'); //加载fs模块,一个异步读取文件的模块。
var thunkify = require('thunkify');//加载thunkify(Thunk的封装)模块
var readFileThunk = thunkify(fs.readFile);//将fs.readFile函数包装成名为readFileThunk的Thunk函数
var gen = function* (){
var r1 = yield readFileThunk('/etc/fstab');//调用readFileThunk,会返回一个函数
console.log(r1.toString());
var r2 = yield readFileThunk('/etc/shells');
console.log(r2.toString());
};
var g = gen();
var r1 = g.next();//执行 readFileThunk('/etc/fstab'),并返回结果,r1.value就是readFileThunk返回的函数
r1.value(function (err, data) {//调用readFileThunk返回的函数,将回调函数传入。
if (err) throw err;
var r2 = g.next(data);//回调函数中继续调用next,并将异步结果传入。
r2.value(function (err, data) {
if (err) throw err;
g.next(data);
});
});
上面代码中可以看到“将同一个回调函数,反复传入next方法的value属性”。也就引出了 Thunk 函数的自动流程管理 :
function run(fn) {//run函数的参数是一个Generator函数
var gen = fn();//得到Generator函数指针
function next(err, data) {//定义next方法
var result = gen.next(data);//调用next,并传入data,result.value为Thunk函数返回的函数。
if (result.done) return;//如果Generator结束了就返回
result.value(next);//否则调用result.value,next函数作为回调函数。这样当得到异步结果之后,就会调用next()
}
next();//执行next方法,第一次err,data都是undefined。
}
function* g() {//Generator函数,要求yield 后面都是Thunk函数。
// ...
}
run(g);//执行run函数,传入Generator函数。
只要定义好Generator函数,编排好异步操作。将Generator函数当做参数传入run,异步操作就可以一个接着一个的执行了。
可能有人问了,这种方式是异步操作串行执行,怎么编排有并行情况的异步呢,答案就是再一次yield中用promise实现并行(ps:自己并没有实验)。
是不是还是不太好理解,自己拿纸画一下就好理解了。我觉得最不好理解的就是Thunk函数,他的返回值还是一个函数。