Generator的异步应用
Promise 的写法只是回调函数的改进,使用then方法以后,异步任务的两段执行看得更清楚了。
Promise 的最大问题是代码冗余,原来的任务被 Promise 包装了一下,不管什么操作,一眼看去都是一堆then,原来的语义变得很不清楚。
那么,有没有更好的写法呢?
传统的编程语言异步解决方案中有一种叫做"协程"(coroutine),意思是多个线程互相协作,完成异步任务。
- 协程A开始执行。
- 协程A执行到一半,进入暂停,执行权转移到协程B。
- (一段时间后)协程B交还执行权。
- 协程A恢复执行。
上面流程的协程A,就是异步任务,因为它分成两段(或多段)执行。
function* asyncJob() {
// ...其他代码
var f = yield readFile(fileA);
// ...其他代码
}
它的奥妙就在其中的yield命令。它表示执行到此处,执行权将交给其他协程。
也就是说,yield命令是异步两个阶段的分界线。
下面看一个异步流程的实战
var fetch = require('node-fetch');
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
var g = gen();
var result = g.next();
result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});
虽然 Generator 函数将异步操作表示得很简洁,但是流程管理却不方便
JavaScript 语言的 Thunk 函数
JavaScript 语言是传值调用,它的 Thunk 函数含义有所不同。在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。
这个单参数版本,就叫做 Thunk 函数。
举个例子:
// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback);
// Thunk版本的readFile(单参数版本)
var Thunk = function (fileName) {
return function (callback) {
return fs.readFile(fileName, callback);
};
};
var readFileThunk = Thunk(fileName);
readFileThunk(callback);
下面是一个简单的 Thunk 函数转换器。
// ES5版本
var Thunk = function(fn){
return function (){
var args = Array.prototype.slice.call(arguments);
return function (callback){
args.push(callback);
return fn.apply(this, args);
}
};
};
// ES6版本
const Thunk = function(fn) {
return function (...args) {
return function (callback) {
return fn.call(this, ...args, callback);
}
};
};
var readFileThunk = Thunk(fs.readFile);
readFileThunk(fileA)(callback);
Thunk 函数现在可以用于 Generator 函数的自动流程管理。
// 我们都知道generator其实是可以自我执行的
// 但是,这不适合异步操作。
function* gen() {
// ...
}
var g = gen();
var res = g.next();
while(!res.done){
console.log(res.value);
res = g.next();
}
再举个实战的例子
var Thunk = function (fileName) {
return function (callback) {
return fs.readFile(fileName, callback);
};
};
var fs = require('fs');
var thunkify = require('thunkify');
var readFileThunk = thunkify(fs.readFile);
var gen = function* (){
var r1 = yield readFileThunk('/etc/fstab');
console.log(r1.toString());
var r2 = yield readFileThunk('/etc/shells');
console.log(r2.toString());
};
var g = gen();
var r1 = g.next();
r1.value(function (err, data) {
if (err) throw err;
var r2 = g.next(data);
r2.value(function (err, data) {
if (err) throw err;
g.next(data);
});
});
我们可以基于以上的流程,自定义一个自动执行的函数,称之为基于 Thunk 函数的 Generator 执行器
function run(fn) {
var gen = fn();
function next(err, data) {
var result = gen.next(data);
if (result.done) return;
result.value(next);
}
next();
}
var g = function* (){
var f1 = yield readFileThunk('fileA');
var f2 = yield readFileThunk('fileB');
// ...
var fn = yield readFileThunk('fileN');
};
run(g);
// 缺点是每一个异步操作,都要是 Thunk 函数
co模块
co模块可以让你不必编写generator执行器
使用
var gen = function* () {
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
var co = require('co');
co(gen);
co(gen).then(function (){
console.log('Generator 函数执行完成');
});
原理
前面说过,Generator 就是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。
两种方法可以做到这一点。
(1)回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。
(2)Promise 对象。将异步操作包装成 Promise 对象,用then方法交回执行权。
co 模块其实就是将两种自动执行器(Thunk 函数和 Promise 对象),包装成一个模块。
使用 co 的前提条件是,Generator 函数的yield命令后面,只能是 Thunk 函数或 Promise 对象。如果数组或对象的成员,全部都是 Promise 对象,也可以使用 co,详见后文的例子。
上一节已经介绍了基于 Thunk 函数的自动执行器。下面来看,基于 Promise 对象的自动执行器。
基于 Promise 对象的自动执行器
先举个例子
var fs = require('fs');
var readFile = function (fileName){
return new Promise(function (resolve, reject){
fs.readFile(fileName, function(error, data){
if (error) return reject(error);
resolve(data);
});
});
};
var gen = function* (){
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
然后依然是手动执行获取灵感
var g = gen();
g.next().value.then(function(data){
g.next(data).value.then(function(data){
g.next(data);
});
});
最终得到
function run(gen){
var g = gen();
function next(data){
var result = g.next(data);
if (result.done) return result.value;
result.value.then(function(data){
next(data);
});
}
next();
}
run(gen);
co模块源码
function co(gen) {
var ctx = this;
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)
+ '"'
)
);
}
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.call(ctx);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
onFulfilled();
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}
});
}