徹底理解thunk函數與co框架

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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章