深入理解generator与co源码解析

初识generator

generator是ES6的新特性,也是一种异步编程的解决方案。generator函数通过function*()来表示,它相当于一个状态机,在内部通过yield来标识每一个状态。generator函数的返回值是一个指向内部状态的指针,我们将它命名为指针g。

在generator函数内部,凡是遇到yield标记的语句都会被阻塞。它们的控制权完全交给指针g,当指针g执行next时,相当于恢复generator的执行,相应的,next操作会返回一个包含value和done的对象。若当前generator执行完成,也就是之后没有yield语句了,value返回undefind,done返回true,告诉next执行完成。如果后面会被阻塞,则done返回false,value则为yield后面语句的值。next可被传入值,所传入的值将作为yield的返回值。具体执行流程可见下图:

generator

注意一种特殊情况: 当generator函数内部有return时,返回{ value: return值, done: true },结束generator的执行。

从异步到同步

开头我们提到generator是异步编程的解决方案,那么generator是如何将异步改变为同步的?我们通过一些简明的generator实例来说明问题。

yield 数字、字符串、普通函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var gen = function* (){
var res1 = yield 1;
console.log(res1);
var res2 = yield '2';
console.log(res2);
var res3 = yield (() => '3')()
console.log(res3);
};
var g = gen();
var value = g.next().value;
while(value) {
value = g.next(value).value;
}

对于数字、字符和普通函数而言,并没有异步同步的概念,需要清楚的是yield后表达式的值只是作为g.next()返回值中的value,不会直接作为yield的返回值。

yield Callback函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var fs = require('fs');
function readFile(fileName) {
return function(callback) {
fs.readFile(fileName, function(err, data) {
callback(err, data.toString());
});
}
}
var gen = function*() {
var res1 = yield readFile('1.txt');
console.log(res1); // 1.txt中的内容
var res2 = yield readFile('2.txt');
console.log(res2); // 2.txt中的内容
return 'test3';
}
var g = gen();
g.next().value(function(err, res) {
g.next(res).value(function(err, res) {
console.log(g.next(res)); // { value: 'test3', done: true }
});
});

一般的callback函数的形式是这样的:

1
2
3
XXX(arg1, arg2, ... , argN, function(err, res) {
...
});

直接在yield后面写这样的函数是丝毫没有作用的,异步依旧是异步。我们所需要做的是将callback返回给next().value,能从中获取异步结果,传给下一个next,这样yield的返回值也就拿到了异步的结果,此时,异步就不知不觉变同步了。

正如上面的readFile的示例一样,我们期望把一般的callback函数转化为以callback为单参数的函数,这就是thunk化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var fs = require('fs');
function thunk(fn) {
return function() {
var args = arguments;
return function(callback) {
var callbackOption = function(err, data) {
callback(err, data.toString());
}
fn.apply(null, [...Array.prototype.slice.call(args), callbackOption]);
}
}
}
var readFile = thunk(fs.readFile);
var gen = function*() {
var res1 = yield readFile('1.txt');
console.log(res1);
var res2 = yield readFile('2.txt');
console.log(res2);
return 'test3';
}
var g = gen();
g.next().value(function(err, res) {
g.next(res).value(function(err, res) {
console.log(g.next(res));
});
});

这里自己写了一个简易实现,更详细的实现可参见node-thunkify源码

yield Promise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const REACT_URL = 'https://www.reddit.com/r/reactjs.json';
const FRONTEND_URL = 'https://www.reddit.com/r/frontend.json';
const request = function(url) {
return new Promise(function (resolve, reject) {
fetch(url)
.then(response => response.json())
.then(json => resolve(json))
});
}
const gen = function* (){
const res1 = yield request(REACT_URL);
console.log(res1);
const res2 = yield request(FRONTEND_URL);
console.log(res2);
};
const g = gen();
g.next().value.then(function(data) {
g.next(data).value.then(function(data) {
g.next(data);
});
});

promise与callback异曲同工,返回一个promise作为value的值,通过reslove获取到异步结果,传给下一个next。

yield generator

利用yield*,来执行generator内嵌的generator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const REACT_URL = 'https://www.reddit.com/r/reactjs.json';
const FRONTEND_URL = 'https://www.reddit.com/r/frontend.json';
const request = function(url) {
return new Promise(function (resolve, reject) {
fetch(url)
.then(response => response.json())
.then(json => resolve(json))
});
}
const gen2 = function* (){
return [
yield request(REACT_URL),
yield request(FRONTEND_URL),
];
}
const gen = function* (){
const res = yield* gen2();
console.log(res);
};
const g = gen();
g.next().value.then(function(data) {
g.next(data).value.then(function(data) {
g.next(data);
});
});

yield 数组

对于数组而言,会并行执行,等待数组中的所有成员均执行完后,在value中返回包含所有结果的数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const REACT_URL = 'https://www.reddit.com/r/reactjs.json';
const FRONTEND_URL = 'https://www.reddit.com/r/frontend.json';
const request = function(url) {
return new Promise(function (resolve, reject) {
fetch(url)
.then(response => response.json())
.then(json => resolve(json))
});
}
const gen = function* (){
const res = yield [
request(REACT_URL),
request(FRONTEND_URL),
];
};
const g = gen();
console.log(g.next()); // { value: [Promise, Promise], done: false }

co的实现

co的本质其实就是对于g.next()的执行和结果传递,yield后面会跟各种不同的数据结构(如: Promise、数组、对象、thunk函数等等),不同的数据结构对于g.next()的执行略微有些区别,co对这个过程进行了封装,让我们能专注于generator函数内部的逻辑编写。

co只接受一个参数,可能是generator function,即function* (),gen.apply执行,可能是generator,即function *()(),执行指针,通过gen.next()判断。

1
2
3
4
5
6
7
8
9
function co(gen) {
var ctx = this;
var args = slice.call(arguments, 1);
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.apply(ctx, args);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
...
}
}

控制generator执行部分,核心思想在于将gen.next的返回结果,传给next函数,首先判断若done为true,则结束整个generator,执行返回promise的resolve,传入最后的value值。若为false,对value值promise化,无论value是什么。value.then注入onFulfilled与onRejected,来执行下一次gen.next。

在generator中,一遇到错误就会通过返回的promise的reject对外抛出异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
onFulfilled();
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
return null;
}
function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} 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(...);
}

co最核心的地方就在于value的promise化,使yield后表达式返回值传递得到统一。

1
2
3
4
5
6
7
8
9
10
11
12
function toPromise(obj) {
if (!obj) return obj;
if (isPromise(obj)) return obj;
if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
if ('function' == typeof obj) return thunkToPromise.call(this, obj);
if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
if (isObject(obj)) return objectToPromise.call(this, obj);
return obj;
}

前三个都比较简单,如果obj未定义,返回undefined,如果本身就是promise,就不需要再转化了,如果是generator或是generator function,那么就在co的内部再执行co,这里可以关注一下是如何判断generator和generator function的。

对于co,我们必须使用thunk化后的function,我们只允许其有一个参数callback,但允许callback有多个参数,但第一个必须为error。

1
2
3
4
5
6
7
8
9
10
function thunkToPromise(fn) {
var ctx = this;
return new Promise(function (resolve, reject) {
fn.call(ctx, function (err, res) {
if (err) return reject(err);
if (arguments.length > 2) res = slice.call(arguments, 1);
resolve(res);
});
});
}

将数组中的所有值均promise化后执行,Promise.all会等待数组内所有promise均fulfilled、或者有一个rejected,才会执行其后的then。

1
2
3
function arrayToPromise(obj) {
return Promise.all(obj.map(toPromise, this));
}

注意在数组和对象中是可以出现一些普通值的,比如数字、对象等等,它们不会被toPromise promise化。

对象与数组也有相似之处,对象通过key进行遍历,对于每个被promise化好的value,都将其存储于promises中,最后Promise.all,生成results。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function objectToPromise(obj){
var results = new obj.constructor();
var keys = Object.keys(obj);
var promises = [];
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var promise = toPromise.call(this, obj[key]);
if (promise && isPromise(promise)) defer(promise, key);
else results[key] = obj[key];
}
return Promise.all(promises).then(function () {
return results;
});
function defer(promise, key) {
// predefine the key in the result
results[key] = undefined;
promises.push(promise.then(function (res) {
results[key] = res;
}));
}
}

最后讲讲co.wrap,co.wrap主要是将co转变为一个返回promise的普通函数,可用于抽象,同时避免相同的co创建新的闭包。有一点切勿混淆,无论是co还是co.wrap, function* ()都是可以传入参数的。

1
2
3
4
5
6
7
8
9
co.wrap = function (fn) {
// 只是保存generatorFunction的引用
createPromise.__generatorFunction__ = fn;
return createPromise;
function createPromise() {
// createPromise的arguments
return co.call(this, fn.apply(this, arguments));
}
};

最近也在看redux-sagaKoa,它们就是基于generator的,等学习完后再进行总结和归纳。

参考资料

ECMAScript6入门-generator

ECMAScript6入门-异步操作