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:1var 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); // avar b = yield readFile('b.txt', {encoding: 'utf8'});console.log(b); // bvar c = yield readFile('c.txt', {encoding: 'utf8'});console.log(c); // creturn 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); // avar b = yield readFile('b.txt', {encoding: 'utf8'});console.log(b); // bvar c = yield readFile('c.txt', {encoding: 'utf8'});console.log(c); // creturn 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 resultresults[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); // avar b = yield readFile('b.txt', {encoding: 'utf8'});console.log(b); // bvar c = yield readFile('c.txt', {encoding: 'utf8'});console.log(c); // creturn 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

彻底理解thunk函数与co框架相关推荐

  1. 深入理解thunk函数

    一.起源 传值调用or传名调用 var x = 1; function f(m){return m * 2; } f(x + 5) // 一种意见是"传值调用"(call by v ...

  2. Thunk 函数的含义和用法

    一.参数的求值策略 Thunk函数早在上个世纪60年代就诞生了. 那时,编程语言刚刚起步,计算机学家还在研究,编译器怎么写比较好.一个争论的焦点是"求值策略",即函数的参数到底应该 ...

  3. es6 --- Thunk函数的作用

    首先了解一下javascript里面的Thunk函数的含义:将多参数函数,替换成一个只接受回调函数作为参数的单参数函数 // 一个具体的例子// 正常版本的readFile(多参数函数) fs.rea ...

  4. javascript之异步操作理解---回调函数,async,await以及promise对象

    javascript之异步操作理解---回调函数,async,await以及promise对象 概述 概述 写在前面:虽然平时做项目,但是发现自己写的代码还是很烂.最近接触了一个对性能要求比较高的项目 ...

  5. 转: ES6异步编程:Thunk函数的含义与用法

    转: ES6异步编程:Thunk函数的含义与用法 参数的求值策略 Thunk函数早在上个世纪60年代就诞生了. 那时,编程语言刚刚起步,计算机学家还在研究,编译器怎么写比较好.一个争论的焦点是&quo ...

  6. 深入理解javascript函数系列第二篇——函数参数

    前面的话 javascript函数的参数与大多数其他语言的函数的参数有所不同.函数不介意传递进来多少个参数,也不在乎传进来的参数是什么数据类型,甚至可以不传参数.本文是深入理解javascript函数 ...

  7. C++深入理解虚函数

    c++深入理解虚函数 虚函数的使用方法: (1)在基类用virtual声明成员函数为虚函数.这样就可以在派生类中重新定义此函数,为它赋予新的功能,并能方便被调用. 在类外定义虚函数时,不必在定义vir ...

  8. 【Python】深入理解Python函数的9个黄金法则

    编程离不开函数.Python的函数除了具备传统意义上的函数特征外,又被赋予了其他一些特性,让它更灵活.更强大.这篇文章结合之前我推荐的一本Python宝书,又添加一些我的实践和理解,总结了深入理解Py ...

  9. python函数可以作为容器对象吗_正确理解Python函数是第一类对象

    正确理解 Python函数,能够帮助我们更好地理解 Python 装饰器.匿名函数(lambda).函数式编程等高阶技术. 函数(Function)作为程序语言中不可或缺的一部分,太稀松平常了.但函数 ...

最新文章

  1. 科学通报:合成微生物群落的构建与应用
  2. 【翻译】SQL Server索引进阶:第八级,唯一索引
  3. html标签article,html标签中section与article 区别
  4. 自动化测试报告(ReportNG)手把手教你
  5. CRMEB系统安装访问不了
  6. nginx 1.8.1安装使用
  7. 小a与黄金街道(欧拉函数)/**模运算规则总结*/
  8. Java动态代理深入解析
  9. Oracle 中国研发中心裁员已成定局,云时代下一个是谁?
  10. setlength java_Java StringBuilder setLength()方法与示例
  11. 产品设计体会(8007)产品经理的主要职责
  12. 摩拜女员工举报前端大佬性骚扰,擅用职权打压同事!
  13. WS以及NW小世界网络的生成(MATLAB)
  14. 微信小程序使用赞赏码功能
  15. snakemake--我最喜欢的流程管理工具
  16. 压缩包设置了解压码忘记了怎么办?
  17. 基于SSM的校园音乐点歌系统平台
  18. qrcode插件生成二维码并下载
  19. MiKTeX + Texmaker ← 推荐的 LaTeX 学习环境
  20. 运维老鸟分享linux运维发展路线规划

热门文章

  1. 使用Javascript制作声音按钮 1
  2. matlab2014simulink中的三相晶闸管整流桥怎么找_三相维也纳 (Vienna) 主拓扑原理、控制及仿真 (下)...
  3. el-input 输入框禁止输入特殊字符
  4. 正则表达式:禁止输入特殊字符和某些特定字符
  5. 学习大数据可以进入哪些高薪行业?
  6. Unicode 编码范围和中文编码范围
  7. multienant oracle_甲骨文(Oracle Cloud)不愧是臭名昭著的垃圾公司
  8. 【Python秘籍】十进制整数与二进制数的转换
  9. 【C++】按字母表的顺序,从字母A到Z顺序打印输出。
  10. PAT甲级 1095