彻底理解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(){
    <pre name="code" class="javascript">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


  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Generator thunk 函数是一个返回 generator 对象的函数,这个 generator 对象可以用来执行异步的操作。 在 JavaScript 中,generator 函数是一种特殊的函数,它可以暂停执行并在需要时恢复执行。Generator 函数可以通过 yield 关键字来暂停执行,并通过 next() 方法来恢复执行。 Generator thunk 函数可以用来处理异步操作的回调函数,它会将回调函数转换成 generator 函数,并将 generator 对象返回给调用者。调用者可以通过调用 generator 对象的 next() 方法来异步执行操作,并通过 yield 关键字来暂停执行。当异步操作完成时,回调函数会将结果作为参数传递给 generator 函数generator 函数再通过调用 next() 方法来恢复执行。 以下是一个示例代码,展示了如何使用 generator thunk 函数来处理异步操作的回调函数: ``` function* asyncOperationThunk(callback) { const result = yield callback; // 暂停执行,等待异步操作完成 return result; } // 异步操作的回调函数 function asyncOperation(callback) { setTimeout(() => { callback('异步操作完成'); }, 1000); } // 使用 generator thunk 函数来执行异步操作 const thunk = asyncOperationThunk(asyncOperation); const iterator = thunk(); // 获取 generator 对象 const next = iterator.next(); // 启动 generator 函数 next.value((result) => { console.log(result); // 输出:异步操作完成 iterator.next(result); // 恢复执行 generator 函数 }); ``` 在上面的代码中,我们首先定义了一个 generator thunk 函数 asyncOperationThunk,它接受一个回调函数作为参数。在 asyncOperationThunk 中,我们使用 yield 关键字来暂停执行,并等待异步操作完成。当异步操作完成时,回调函数会将结果作为参数传递给 generator 函数generator 函数再通过调用 next() 方法来恢复执行。 接下来,我们定义了一个异步操作的回调函数 asyncOperation,并将它传递给 asyncOperationThunk 函数。我们使用 asyncOperationThunk 函数来创建一个 thunk 函数,并通过调用它来获取 generator 对象和启动 generator 函数。 最后,我们通过调用 generator 对象的 next() 方法来异步执行操作,并通过回调函数来获取操作结果。当操作完成时,我们再次调用 generator 对象的 next() 方法来恢复执行 generator 函数,并将操作结果作为参数传递给它。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值