本文始发于个人我的博客,如需转载请注明出处。
为了更好的阅读体验,能够直接进去个人我的博客看。html
阅读本文须要对Generator
和Promise
有一个基本的了解。node
这里我简单地介绍一下二者的用法。git
关于Generator的用法,推荐MDN上面的解释function *函数,里面很是详细。github
用一句话总结就是,generator函数
是回调地狱的一种解决方案,它跟promise
相似,可是却能够以同步的方式来书写代码,而避免了promise的链式调用。api
它的执行过程在于调用生成器函数(generator function)
后,会返回一个iterator(迭代)对象
,即Generator对象
,可是它并不会马上执行里面的代码。数组
它有几个方法,next()
, throw()
和return()
。调用next()方法后,它会找到第一个yield关键字(直到找到程序底部或者return语句),每次程序运行到yield关键字时,程序便会暂停,保存当前环境里面的变量的值,而后能够跳出当前运行环境去执行yield后面的代码,再把结果返回回来。promise
返回的结果是一个对象,相似于{value: '', done: false}
, value表示本次yield后面执行以后返回的结果。若是是Promise实例,则是返回resolved后的值。done表示迭代器是否执行完毕,若为true
,则表示当前生成器函数已经产生了最后输出的值,即生成器函数已经返回。app
下面是一个简单的例子:框架
const gen = function *() { let index = 0; while(index < 3) yield index++; return 'All done.' }; const g = gen(); console.log(g.constructor); // output: GeneratorFunction {} console.log(g.next()); // output: { value: 0, done: false } console.log(g.next()); // output: { value: 1, done: false } console.log(g.next()); // output: { value: 2, done: false } console.log(g.next()); // output: { value: 'All done.', done: true } console.log(g.next()); // output: { value: undefined, done: true }
关于Promise
的用法,能够查阅我以前写过的一篇文章《关于ES6中Promise的用法》,写得比较详细。koa
Promise对象用于一个异步操做的最终完成(或失败)及其结果值的表示(简单点说就是处理异步请求)。Promise核心就在于里面状态的变换,是rejected
、resolved
仍是pending
,还有就是原型链上的then()
方法,它能够传递本次状态转换后返回的值。
因为实际须要,这几天学习了koa2.x
框架,可是它已经不推荐使用generator函数了,推荐用async/await
组合。
koa2.x的最新用法:
async/await(node v7.6+):
const Koa = require('koa'); const app = new Koa(); app.use(async (ctx, next) => { const start = Date.now(); await next(); const ms = Date.now() - start; console.log(`${ctx.method} ${ctx.url} - ${ms}ms`); });
common 用法:
const Koa = require('koa'); const app = new Koa(); // response app.use(ctx => { ctx.body = 'Hello Koa'; }); app.listen(3000);
因为本地的Node版本是v6.11.5
,而使用async/await则须要Node版本v7.6
以上,因此我想有没有什么模块可以把koa2.x版本的语法兼容koa1.x的语法。koa1.x语法的关键在于generator/yield
组合。经过yield能够很方便地暂停程序的执行,并改变执行环境。
这时候我找到了TJ大神写的co模块
,它可让异步流程同步化,还有koa-convert
模块等等,这里着重介绍co模块。
co在koa2.x里面的用法以下:
const Koa = require('koa'); const app = new Koa(); const co = require('co'); // response app.use(co.wrap(function *(ctx, next) { yield next(); // yield someAyncOperation; // ... ctx.body = 'co'; })); app.listen(3000);
co模块不只能够配合koa框架充当中间件的转换函数使用,还支持批量执行generator函数,这样就无需手动调用屡次next()来获取结果了。
它支持的参数有函数、promise、generator、数组和对象
。
// co的源码 return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, ' + 'but the following object was passed: "' + String(ret.value) + '"'));
下面举一个co传递进来一个generator函数的例子:
// 这里模拟一个generator函数调用 const co = require('co'); co(gen).then(data => { // output: then: ALL Done. console.log('then: ' + data); }); function *gen() { let data1 = yield pro1(); // output: pro1 had resolved, data1 = I am promise1 console.log('pro1 had resolved, data1 = ' + data1); let data2 = yield pro2(); // output: pro2 had resolved, data2 = I am promise2 console.log('pro2 had resolved, data2 = ' + data2); return 'ALL Done.' } function pro1() { return new Promise((resolve, reject) => { setTimeout(resolve, 2000, 'I am promise1'); }); } function pro2() { return new Promise((resolve, reject) => { setTimeout(resolve, 1000, 'I am promise2'); }); }
我以为co()函数很神奇,里面究竟通过了什么样的转换?抱着一颗好奇心,读了一下co的源码。
co函数调用后,返回一个Promise实例。
co的思想就是将一个传递进来的参数进行合法化,再经过转换成Promise实例返回出去。若是参数fn是generator函数
的话,里面还能够自动进行遍历,执行generator函数里面的yield关键字后面的内容,并返回结果,也就是不断地调用fn().next()
方法,再经过传递返回的Promise实例resolved
后的值,从而达到同步执行generator函数的效果。
这里要注意,co里面最主要的是要理解Promise实例和Generator对象,它们是co函数里面的程序自动遍历执行的关键。
下面解释一下co模块里面的最重要的两部分,一个是generator函数的自动调用,另一个是参数的Promise化。
第一,generator函数的自动调用(中文部分是个人解释):
function co(gen) { // 保存当前的执行环境 var ctx = this; // 切割出函数调用时传递的参数 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实例 return new Promise(function(resolve, reject) { // 若是gen是一个函数,则返回一个新的gen函数的副本, // 里面绑定了this的指向,即ctx if (typeof gen === 'function') gen = gen.apply(ctx, args); // 若是gen不存在或者gen.next不是一个函数 // 就说明gen已经调用完成, // 那么直接能够resolve(gen),返回Promise if (!gen || typeof gen.next !== 'function') return resolve(gen); // 首次调用gen.next()函数,假如存在的话 onFulfilled(); /** * @param {Mixed} res * @return {Promise} * @api private */ function onFulfilled(res) { var ret; try { // 尝试着获取下一个yield后面代码执行后返回的值 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 */ // 这个next()函数是最为关键的一部分, // 里面几乎包含了generator自动调用实现的核心 function next(ret) { // 若是ret.done === true, // 证实generator函数已经执行完毕 // 即已经返回了值 if (ret.done) return resolve(ret.value); // 把ret.value转换成Promise对象继续调用 var value = toPromise.call(ctx, ret.value); // 若是存在,则把控制权交给onFulfilled和onRejected, // 实现递归调用 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) + '"')); } }); }
对于以上代码中的onFulfilled
和onRejected
,咱们能够把它们当作是co模块对于resolve
和reject
封装的增强版。
第二,参数Promise化,咱们来看一下co中的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; }
toPromise的本质上就是经过断定参数的类型,而后再经过转移控制权给不一样的参数处理函数,从而获取到指望返回的值。
关于参数的类型的判断,看一下源码就能理解了,比较简单。
咱们着重来分析一下objectToPromise的实现:
function objectToPromise(obj){ // 获取一个和传入的对象同样构造器的对象 var results = new obj.constructor(); // 获取对象的全部能够遍历的key var keys = Object.keys(obj); var promises = []; for (var i = 0; i < keys.length; i++) { var key = keys[i]; // 对于数组的每个项都调用一次toPromise方法,变成Promise对象 var promise = toPromise.call(this, obj[key]); // 若是里面是Promise对象的话,则取出e里面resolved后的值 if (promise && isPromise(promise)) defer(promise, key); else results[key] = obj[key]; } // 并行,按顺序返回结果,返回一个数组 return Promise.all(promises).then(function () { return results; }); // 根据key来获取Promise实例resolved后的结果, // 从而push进结果数组results中 function defer(promise, key) { // predefine the key in the result results[key] = undefined; promises.push(promise.then(function (res) { results[key] = res; })); } }
上面理解的关键就在于把key遍历,若是key
对应的value
也是Promise
对象的话,那么调用defer()
方法来获取resolved
后的值。
经过以上的简单介绍,咱们就能够尝试来写一个属于本身的generator函数运行器了,目标功能是可以自动运行function*
函数,而且里面的yield子句
后面跟着的都是Promise实例
。
具体代码(my-co.js
)以下:
// my-co.js module.exports = my-co; let my-co = function (gen) { // gen是一个具备Promise的生成器函数 const g = gen(); // 迭代器 // 首次调用next next(); function next(val) { let ret = g.next(val); // 调用ret if (ret.done) { return ret.value; } if (ret && 'function' === typeof ret.value.then) { ret.value.then( (data) => { // 继续循环下去 return next(data); // promise resolved }); } } };
这样咱们就能够在test.js
文件中调用了:
// test.js const myCo = require('./my-co'); const fs = require('fs'); let gen = function *() { let data1 = yield pro1(); console.log('data1: ' + data1); let data2 = yield pro2(); console.log('data2: ' + data2); let data3 = yield pro3(); console.log('data3: ' + data3); let data4 = yield pro4(data1 + '\n' + data2 + '\n' + data3); console.log('data4: ' + data4); return 'All done.' }; // 调用myCo myCo(gen); // 延迟两秒resolve function pro1() { return new Promise((resolve, reject) => { setTimeout(resolve, 2000, 'promise1 resolved'); }); } // 延迟一秒resolve function pro2() { return new Promise((resolve, reject) => { setTimeout(resolve, 1000, 'promise2 resolved'); }); } // 写入Hello World到./1.txt文件中 function pro3() { return new Promise((resolve, reject) => { fs.appendFile('./1.txt', 'Hello World\n', function(err) { resolve('write-1 success'); }); }); } // 写入content到./1.txt文件中 function pro4(content) { return new Promise((resolve, reject) => { fs.appendFile('./1.txt', content, function(err) { resolve('write-2 success'); }); }); }
控制台输出结果:
// output data1: promise1 resolved data2: promise2 resolved data3: write-1 success data4: write-2 success
./1.txt
文件内容:
Hello World promise1 resolved promise2 resolved write-1 success
由上可知,运行的结果符合咱们的指望。
虽然这个运行器很简单,后面只支持Promise实例,而且也不支持多种参数,可是却引导出了一个思路,促使咱们思考怎么去展现咱们的代码,还有就是颇有效地避免了多重then,以同步的方式来书写异步代码。Promise解决的是回调地狱
的问题(callback hell
),而Generator解决的是代码的书写方式。孰优孰劣,全在于我的意愿。
以上分析了co部分源码的精髓,讲到了co函数里面generator函数自动遍历执行的机制,还讲到了co里面最为关键的objectToPromise()
方法。
在文章的后面咱们编写了一个属于本身的generator函数遍历器,其中主要的是next()方法,它能够检测咱们yield后面Promise操做是否完成。若是generator的状态done
尚未置为true
,那么继续调用next(val)
方法,并把上一次yield
操做获取到的值传递下去。
有时候在引用别人的模块出现问题时,若是在网上找不到本身指望的答案,那么咱们能够根据本身的能力来选择性地分析一下做者的源码,看源码是一种很好的成长方式。
坦白说,这是我第一次深刻分析模块的源码,co模块的源码包括注释和空行只有230多行
左右,因此这是一个很好的切入点。里面代码虽少,可是理解却不易。
若是以上所述有什么问题,欢迎反馈。
感谢支持。