ES6帶來了很多新的特性,其中生成器、yield等能對之前金字塔式的異步回調做到很好地解決,而基於此封裝的co框架能讓我們完全已同步的方式來編寫異步代碼。這篇文章就對生成器函數(GeneratorFunction)及框架thunkify、co的核心代碼做比較徹底的分析。co的使用還是比較廣泛的,除了我們日常的編碼要用到外,一些知名框架也是基於co實現的,比如被稱爲下一代的Nodejs web框架的koa等。
生成器函數
生成器函數是寫成:
function* func(){}
格式的代碼,其本質也是一個函數,所以它具備普通函數所具有的所有特性。除此之外,它還具有以下有用特性:
1. 執行生成器函數後返回一個生成器(Generator),且生成器具有throw()方法,可手動拋出一個異常,也常被用於判斷是否是生成器;
2. 在生成器函數內部可以使用yield(或者yield*),函數執行到yield的時候都會暫停執行,並返回yield的右值(函數上下文,如變量的綁定等信息會保留),通過生成器的next()方法會返回一個對象,含當前yield右邊表達式的值(value屬性),以及generator函數是否已經執行完(done屬性)等的信息。每次執行next()方法,都會從上次執行的yield的地方往下,直到遇到下一個yield並返回包含相關執行信息的對象後暫停,然後等待下一個next()的執行;
3. 生成器的next()方法返回的是包含yield右邊表達式值及是否執行完畢信息的對象;而next()方法的參數是上一個暫停處yield的返回值。
下面用例子說明:
例1:
function test(){
return 'b';
}
function* func(){
var a = yield 'a';
console.log('gen:',a);// gen: undefined
var b = yield test();
console.log('gen:',b);// gen: undefined
}
var func1 = func();
var a = func1.next();
console.log('next:', a);// next: { value: 'a', done: false }
var b = func1.next();
console.log('next:', b);// next: { value: 'b', done: false }
var c = func1.next();
console.log('next:', c);// next: { value: undefined, done: true }
根據上面說過的第3條執行準則:“生成器的next()方法返回的是包含yield右邊表達式值及是否執行完畢信息的對象;而next()方法的參數是上一個暫停處yield的返回值”,因爲我們沒有往生成器的next()中傳入任何值,所以:var a = yield ‘a’;中a的值爲undefined。
那我們可以將例子稍微修改下:
例2:
function test(){
return 'b';
}
function* func(){
var a = yield 'a';
console.log('gen:',a);// gen:1
var b = yield test();
console.log('gen:',b);// gen:2
}
var func2 = func();
var a = func2.next();
console.log('next:', a);// next: { value: 'a', done: false }
var b = func2.next(1);
console.log('next:', b);// next: { value: 'b', done: false }
var c = func2.next(2);
console.log('next:', c);// next: { value: undefined, done: true }
這個就比較清晰明瞭了,不再做過多解釋。
關於yield*
yield暫停執行並只返回右值,而yield*則將函數委託到另一個生成器或可迭代的對象(如:字符串、數組、類數組以及ES6的Map、Set等)。舉例如下:
arguments
function* genFunc(){
yield arguments;
yield* arguments;
}
var gen = genFunc(1,2);
console.log(gen.next().value); // { '0': 1, '1': 2 }
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
Generator
function* gen1(){
yield 2;
yield 3;
}
function* gen2(){
yield 1;
yield* gen1();
yield 4;
}
var g2 = gen2();
console.log(g2.next().value); // 1
console.log(g2.next().value); // 2
console.log(g2.next().value); // 3
console.log(g2.next().value); // 4
thunk函數
在co的應用中,爲了能像寫同步代碼那樣書寫異步代碼,比較多的使用方式是使用thunk函數(但不是唯一方式,還可以是:Promise)。比如讀取文件內容的一步函數fs.readFile()方法,轉化爲thunk函數的方式如下:
function readFile(path, encoding){
return function(cb){
fs.readFile(path, encoding, cb);
};
}
那什麼叫thunk函數呢?
thunk函數具備以下兩個要素:
1. 有且只有一個參數是callback的函數;
2. callback的第一個參數是error。
使用thunk函數,同時結合co我們就可以像寫同步代碼那樣來寫書寫異步代碼,先來個例子感受下:
var co = require('co'),
fs = require('fs'),
Promise = require('es6-promise').Promise;
function readFile(path, encoding){
return function(cb){
fs.readFile(path, encoding, cb);
};
}
co(function* (){// 外面不可見,但在co內部其實已經轉化成了promise.then().then()..鏈式調用的形式
var a = yield readFile('a.txt', {encoding: 'utf8'});
console.log(a); // a
var b = yield readFile('b.txt', {encoding: 'utf8'});
console.log(b); // b
var c = yield readFile('c.txt', {encoding: 'utf8'});
console.log(c); // c
return yield Promise.resolve(a+b+c);
}).then(function(val){
console.log(val); // abc
}).catch(function(error){
console.log(error);
});
是不是很酷?真的很酷!
其實,對於每次都去自己書寫一個thunk函數還是比較麻煩的,有一個框架thunkify可以幫我們輕鬆實現,修改後的代碼如下:
var co = require('co'),
thunkify = require('thunkify'),
fs = require('fs'),
Promise = require('es6-promise').Promise;
var readFile = thunkify(fs.readFile);
co(function* (){// 外面不可見,但在co內部其實已經轉化成了promise.then().then()..鏈式調用的形式
var a = yield readFile('a.txt', {encoding: 'utf8'});
console.log(a); // a
var b = yield readFile('b.txt', {encoding: 'utf8'});
console.log(b); // b
var c = yield readFile('c.txt', {encoding: 'utf8'});
console.log(c); // c
return yield Promise.resolve(a+b+c);
}).then(function(val){
console.log(val); // abc
}).catch(function(error){
console.log(error);
});
對於thunkify的實現,大概的註釋如下:
/**
* Module dependencies.
*/
var assert = require('assert');
/**
* Expose `thunkify()`.
*/
module.exports = thunkify;
/**
* Wrap a regular callback `fn` as a thunk.
*
* @param {Function} fn
* @return {Function}
* @api public
*/
function thunkify(fn) {
assert('function' == typeof fn, 'function required');
// 返回一個包含thunk函數的函數,返回的thunk函數用於執行yield,而外圍這個函數用於給thunk函數傳遞參數
return function() {
var args = new Array(arguments.length);
// 緩存當前上下文環境,給fn提供執行環境
var ctx = this;
// 將參數類數組轉化爲數組(實現方式略顯臃腫,可直接用Array.prototype.slice.call(arguments)實現)
for (var i = 0; i < args.length; ++i) {
args[i] = arguments[i];
}
// 真正的thunk函數(有且只有一個參數是callback的函數,且callback的第一個參數爲error)
// 類似於:
// function(cb) {fs.readFile(path, {encoding: 'utf8}, cb)}
return function(done) {
var called;
// 將回調函數再包裹一層,避免重複調用;同時,將包裹了的真正的回調函數push進參數數組
args.push(function() {
if (called) return;
called = true;
done.apply(null, arguments);
});
try {
// 在ctx上下文執行fn(一般是異步函數,如:fs.readFile)
// 並將執行thunkify之後返回的函數的參數(含done回調)傳入,類似於執行:
// fs.readFile(path, {encoding: 'utf8}, done)
// 關於done是做什麼用,則是在co庫內
fn.apply(ctx, args);
} catch (err) {
done(err);
}
}
}
};
代碼並不複雜,看註釋應該就能看懂了。
co框架
我們將整個框架先列出在下面:
/**
* slice() reference.
*/
var slice = Array.prototype.slice;
/**
* Expose `co`.
*/
module.exports = co['default'] = co.co = co;
/**
* Wrap the given generator `fn` into a
* function that returns a promise.
* This is a separate function so that
* every `co()` call doesn't create a new,
* unnecessary closure.
*
* @param {GeneratorFunction} fn
* @return {Function}
* @api public
*/
co.wrap = function(fn) {
createPromise.__generatorFunction__ = fn;
return createPromise;
function createPromise() {
return co.call(this, fn.apply(this, arguments));
}
};
/**
* Execute the generator function or a generator
* and return a promise.
*
* @param {Function} fn
* @return {Promise}
* @api public
*/
// gen必須是一個生成器函數(會執行該函數並返回生成器)或者是一個生成器(generator函數的返回值)
function co(gen) {
// 記錄上下文環境
var ctx = this;
// 除gen之外的其他參數
var args = slice.call(arguments, 1)
// we wrap everything in a promise to avoid promise chaining,
// which leads to memory leak errors.
// see https://github.com/tj/co/issues/180
// 返回一個Promise實例,所以可以以下面這種方式調用co:
/**
* co(function*(){}).then(function(val){
*
* });
* */
return new Promise(function(resolve, reject) {
// 如果gen是一個函數則將其置爲函數的返回值
if (typeof gen === 'function') {
gen = gen.apply(ctx, args);
}
// 如果gen不是生成器,則直接返回
if (!gen || typeof gen.next !== 'function') {
return resolve(gen);
}
// 核心方法,啓動generator的執行
onFulfilled();
/**
* @param {Mixed} res
* @return {Promise}
* @api private
*/
// res記錄的是:上一個yield的返回值中value的值({done:false,value:''}中value的值)
// ret記錄的是:本次yield的返回值(整個{done:false,value:''})
// generator相關:執行生成器的next()方法的時候,會在當前yield處執行完畢並停住,
// next()方法返回的是yield執行後的狀態(done)及yield 表達式返回的值(value),
// 而next()方法內的參數會作爲:var a=yield cb();a的值,所以往下看
/**
* 假設:co(function*(){
* var a = yield readFile('a.txt');
* console.log(a);
* var b = yield readFile('b.txt);
* console.log(b);
* });
* 那麼根據上面generator的理論,res就是a,b的值
* */
function onFulfilled(res) {
var ret;
try {
// 返回的是co裏yield後面表達式的值。如果co裏yield的是thunk函數那ret.value就是thunk函數
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}
/**
* @param {Error} err
* @return {Promise}
* @api private
*/
function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}
/**
* Get the next value in the generator,
* return a promise.
*
* @param {Object} ret
* @return {Promise}
* @api private
*/
function next(ret) {
// 執行完畢的話,如果外層調用的是:
/**
* co(function*(){
* return yield Promise.resolve(1);
* }).then(function(val){
* console.log(val); // 1
* });
* */
// 則ret.value就是上面傳遞到then成功回調裏val的值
if (ret.done) {
return resolve(ret.value);
}
// 還沒結束的話將ret.value轉化爲Promise實例,相當於執行:
// promise.then(onFulfilled).then(onFulfilled).then(onFulfilled)...
var value = toPromise.call(ctx, ret.value);
if (value && isPromise(value)) {
// 此時onFulfilled裏參數傳入的就是上一個yield的返回值的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) + '"'));
}
});
}
/**
* Convert a `yield`ed value into a promise.
*
* @param {Mixed} obj
* @return {Promise}
* @api private
*/
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;
}
/**
* Convert a thunk to a promise.
*
* @param {Function}
* @return {Promise}
* @api private
*/
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);
});
});
}
/**
* Convert an array of "yieldables" to a promise.
* Uses `Promise.all()` internally.
*
* @param {Array} obj
* @return {Promise}
* @api private
*/
function arrayToPromise(obj) {
return Promise.all(obj.map(toPromise, this));
}
/**
* Convert an object of "yieldables" to a promise.
* Uses `Promise.all()` internally.
*
* @param {Object} obj
* @return {Promise}
* @api private
*/
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;
}));
}
}
/**
* Check if `obj` is a promise.
*
* @param {Object} obj
* @return {Boolean}
* @api private
*/
function isPromise(obj) {
return 'function' == typeof obj.then;
}
/**
* Check if `obj` is a generator.
*
* @param {Mixed} obj
* @return {Boolean}
* @api private
*/
function isGenerator(obj) {
return 'function' == typeof obj.next && 'function' == typeof obj.throw;
}
/**
* Check if `obj` is a generator function.
*
* @param {Mixed} obj
* @return {Boolean}
* @api private
*/
function isGeneratorFunction(obj) {
var constructor = obj.constructor;
if (!constructor) return false;
if ('GeneratorFunction' === constructor.name || 'GeneratorFunction' === constructor.displayName) return true;
return isGenerator(constructor.prototype);
}
/**
* Check for plain object.
*
* @param {Mixed} val
* @return {Boolean}
* @api private
*/
function isObject(val) {
return Object == val.constructor;
}
對於核心部分,我做註釋。下面,我們基於我們之前的例子對co的執行流程做一下分析。
我們的例子是:
var co = require('co'),
thunkify = require('thunkify'),
fs = require('fs'),
Promise = require('es6-promise').Promise;
function readFile(path, encoding){
return function(cb){
fs.readFile(path, encoding, cb);
};
}
//var readFile = thunkify(fs.readFile);
co(function* (){// 外面不可見,但在co內部其實已經轉化成了promise.then().then()..鏈式調用的形式
var a = yield readFile('a.txt', {encoding: 'utf8'});
console.log(a); // a
var b = yield readFile('b.txt', {encoding: 'utf8'});
console.log(b); // b
var c = yield readFile('c.txt', {encoding: 'utf8'});
console.log(c); // c
return yield Promise.resolve(a+b+c);
}).then(function(val){
console.log(val); // abc
}).catch(function(error){
console.log(error);
});
首先,執行co()函數,內部除了緩存當前執行上下文環境、除generator函數之外的參數處理,主要返回一個Promise實例:
// 記錄上下文環境
var ctx = this;
// 除gen之外的其他參數
var args = slice.call(arguments, 1)
// we wrap everything in a promise to avoid promise chaining,
// which leads to memory leak errors.
// see https://github.com/tj/co/issues/180
// 返回一個Promise實例,所以可以以下面這種方式調用co:
/**
* co(function*(){}).then(function(val){
*
* });
* */
return new Promise(function(resolve, reject) {
});
我們主要看這個Promise內部做了什麼。
if (typeof gen === 'function') {
gen = gen.apply(ctx, args);
}
首先,判斷co()函數的第一個參數是否是函數,是的話將除gen之外的參數傳給該函數並返回給gen;在這裏因爲gen是一個生成器函數,所以返回一個生成器;
if (!gen || typeof gen.next !== 'function') {
return resolve(gen);
}
後面判斷如果gen此時不是一個生成器,則直接執行Promise的resolve,其實就是將gen傳回給:co().then(function(val){});裏的val了;
我們這個例子gen是一個生成器,則繼續往下執行。
onFulfilled();
後面我們就遇到了co的核心函數:onFulfilled。我們看下這個函數做了什麼。
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}
爲了防止分心,裏面錯誤的處理我們先暫時不理。
第一次執行該方法,res值爲undefined,然後執行生成器的next()方法,對應我們例子裏就是執行:
var a = yield readFile('a.txt', {encoding: 'utf8'});
那麼ret是一個對象,大概是這樣:
{
done: false,
value: function(cb){
fs.readFile(path, encoding, cb);
}
}
然後將ret傳給next函數。next函數是:
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) + '"'));
}
首先判斷生成器內部是否已經執行完,執行完則將執行結果resolve出去。很明顯我們例子裏才執行到第一個yield,並沒有執行完。沒執行完,則將ret.value轉化爲一個Promise實例,我們這裏是一個thunk函數,所以toPromise真正執行的是:
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;
}
/**
* Convert a thunk to a promise.
*
* @param {Function}
* @return {Promise}
* @api private
*/
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實例。而這裏面,也對fn做了執行,fn是:function(cb){},對應到這裏,function(err, res){…}就是被傳入到fn中的cb,第一個參數就是error對象,第二個參數res就是讀取文件後數據,然後執行resolve,將結果傳到下一個then方法的成功函數內,而在這裏對應的是:
if (value && isPromise(value)) {
return value.then(onFulfilled, onRejected);
}
其實也就是onFulFilled的參數res。根據上面第三條執行準則,我們知道,res是被傳入到生成器的next()方法裏的,其實也就是對應co內生成器函數參數裏的var a = yield readFile(‘a.txt’,{encoding:’utf8’});裏的a的值,從而實現了類似於同步的變成範式。
這樣,整個基於thunk函數的co框架編程也就理通了,其他的Promise、Generator、GeneratorFunction、Object、Array模式的類似,不再做過多分析。
理解了co的執行邏輯,我們就能更好的掌握其用法,對於後續使用koa等基於co編寫的框架我們也能更快速地上手。
co的簡版
爲了更方便快捷的理解co的執行邏輯,在網絡上還有一個簡版的實現,如下:
function co(generator) {
return function(fn) {
var gen = generator();
function next(err, result) {
if(err){
return fn(err);
}
var step = gen.next(result);
if (!step.done) {
step.value(next);
} else {
fn(null, step.value);
}
}
next();
}
}
但這個實現,僅支持yield後面是thunk函數的情形。使用示例:
var co = require('./co');
// wrap the function to thunk
function readFile(filename) {// 輔助傳參,yield真正使用的是其返回的thunk函數
return function(callback) {
require('fs').readFile(filename, 'utf8', callback);
};
}
co(function * () {
var file1 = yield readFile('./file/a.txt');
var file2 = yield readFile('./file/b.txt');
console.log(file1);
console.log(file2);
return 'done';
})(function(err, result) {
console.log(result)
});
會打印出:
content in a.txt
content in b.txt
done