JavaScript 异步编程–await实现原理
Generator(生成器)是ES6标准引入的新的数据类型,其最大的特点就是可以交出函数的执行的控制权,即:通过yield关键字标明需要暂停的语句,执行时遇到yield语句则返回该语句执行结果,等到调用next函数时(也就是说可以通过控制调用next函数的时机达到控制generator执行的目的)重新回到暂停的地方往下执行,直至generator执行结束。
基本结构
以下是一个典型的generator函数的示例,以"*"标明为generator。
function* gen(x){
var y = yield x + 2;
console.log(y); // undefine
var yy = yield x + 3;
console.log(yy); // 6
return y; // 没啥用
}
var g = gen(1);
var r1 = g.next();
console.log(r1); // { value: 3, done: false }
var r2 = g.next();
console.log(r2); // { value: 4, done: false }
var r3 = g.next(6);
console.log(r3); // { value: undefined, done: true }
上述代码中,调用gen函数,会返回一个内部指针(即遍历器)g,这是Generator函数和一般函数不同的地方,调用它不会返回结果,而是一个指针对象。调用指针g的next方法,会移动内部指针,指向第一个遇到的yield语句,上例就是执行到x+2为止。换言之,next方法的作用是分阶段执行Generator函数。每次调用next方法,会返回一个对象{value: any, done: boolean},表示当前阶段的信息,其中value属性是yield语句后面表达式的值;done属性是一个布尔值,表示Generator函数是否执行完毕,即是否还有下一个阶段。next方法输入参数即为yield语句的值,因此生成器gen中y为第二次调用next的输入参数"undefine",yy为第三次调用next的输入参数6。
总结:
- generator返回遍历器,可遍历所有yield
- yield将生成器内部代码分割成n段,通过调用next方法一段一段执行
- next方法返回的value属性向外输出数据,next方法通过实参向生成器内部输入数据
##思考
如果yield标记的语句是个异步执行的函数func,然后在func回调中调用next,则实现了等待func异步执行的效果-----“func要做的事做完了,才会往下走”,这样就避免了多重回调嵌套(callback hell,回调地狱, 如下所示)
func1(function (res) {
// do something
func2(function (res2) {
// do something
func3(function (res3) {
// do something
})
})
})
##Thunk函数
什么是thunk函数?详见Thunk 函数的含义和用法
简单理解:thunk函数利用闭包可以缓存状态的特性,先传参数,再执行函数,这样就将函数调用过程分成了两步。以下thunkify函数可将普通异步函数转化为thunk函数。
function thunkify(fn) {
assert('function' == typeof fn, 'function required');
return function () {
// arguments为异步函数的参数(不包含回调函数参数)
var args = new Array(arguments.length);
var ctx = this;
for (var i = 0; i < args.length; ++i) {
args[i] = arguments[i];
}
// done为异步函数的回调函数(callback)
return function (done) {
var called;
args.push(function () {
if (called) return;
called = true;
done.apply(null, arguments);
});
try {
// 到这里,异步函数才真正被调用
fn.apply(ctx, args);
} catch (err) {
done(err);
}
}
}
};
##Generator执行控制
thunk函数有什么用呢?其一个典型应用就是用于控制generator的执行,见如下示例是为了实现多个文件的顺序读取,实现了同步写法,避免回调嵌套。
const fs = require('fs');
const readFileThunk = thunkify(fs.readFile);
var generator = function* () {
for (var i = 0; i < arguments.length; i++) {
console.log('file: %s', arguments[i]);
// yield 返回thunkify最内部的 function (done){} 函数,此处传入了readFile函数参数,但并没有执行
var r1 = yield readFileThunk(arguments[i], 'utf8');
console.log('r1: %s', r1);
}
}
function rungenerator(generator) {
//文件名称
var args = [];
for (var i = 1; i < arguments.length; i++) {
args.push(arguments[i]);
}
//生成generator实例
var gen = generator.apply(null, args);
function done(err, data) {
//执行跳到 generator中去
var result = gen.next(data);
if (result.done) { return; }
// 此处才是真正的调用readFile函数开始读取文件内容,done作为回调, 文件读取完成后,执行gen.next(),
// 告诉generator继续执行,并通过yield返回下一个thunk函数,开始读取下一个文件,从而达到顺序执行的效果
result.value(done);
}
done();
}
rungenerator(generator, '123.txt', '1234.txt', 'he.txt')
上述代码中,rungenerator是一个执行generator的函数,具有通用性,封装下就成了co库----generator函数自动执行的解决方案。
var fs = require('fs');
var co = require('co');
var thunkify = require('thunkify');
var readFile = thunkify(fs.readFile);
co(function*(){
var files=['./text1.txt', './text2.txt', './text3.txt'];
var p1 = yield readFile(files[0]);
console.log(files[0] + ' ->' + p1);
var p2 = yield readFile(files[1]);
console.log(files[1] + ' ->' + p2);
var p3 = yield readFile(files[2]);
console.log(files[2] + ' ->' + p3);
return 'done';
});
看起来舒服多了。。。
##async和await
async和await是ES7中的新语法,实际上是generator函数的语法糖
Nodejs最新版已经支持了,浏览器不支持的,可以用Babel转下。
var fs = require('fs');
var readFile = function (fileName){
return new Promise(function (resolve, reject){
fs.readFile(fileName, function(error, data){
if (error){
reject(error);
}
else {
resolve(data);
}
});
});
};
var asyncReadFile = async function (){
var f1 = await readFile('./text1.txt');
var f2 = await readFile('./text2.txt');
console.log(f1.toString());
console.log(f2.toString());
};
asyncReadFile();
##致谢
主要学习了nullcc的博客《深入解析Javascript异步编程》、无脑的博客《node的 thunkify模块说明》、阮一峰老师的博客《Thunk 函数的含义和用法》,由衷地感谢以上作者!!!!!