目前咱们对异步回调的解决方案有这么几种:回调,deferred/promise和事件触发。回调的方式自没必要说,须要硬编码调用,并且有可能会出现复杂的嵌套关系,形成“回调黑洞”;deferred/promise方式则对使用者而言简洁明了,在执行异步函数以前就已经构造好了执行链--then链,并且实现也很灵活,具体可参考Promise的实现;事件机制则是一种观察者模式的实现,但也必须硬编码在异步执行的函数中,当异步函数执行完毕后再trigger相关事件,而观察者则相应执行事件处理函数。
注意,刚刚提到了一个词--硬编码,依赖这种方式仅实现回调局限性很大,如在node中,对fs.readFile('file1','utf-8')完成以后再进行fs.readFile('file2','utf-8'),使用回调和事件触发则必须在第一个异步的回调函数中进行调用trigger,加强了这两个操做的强依赖,使用deferred/promise则会很好的避免。
如今,随着ECMAScript6的逐渐普及,咱们能够在chrome和node端尝试一种新的异步流程控制--generator。经过generator,咱们能够控制函数内部的执行阶段,进而能够利用高阶函数的特性进行扩展,完成对异步流程的控制。html
因为隶属于ECMAScript6规范,所以兼容性是一个大问题,不过咱们在最新版的chrome和node --harmony下使用该功能,因此作node端开发的小伙伴们能够大胆的使用。
那么,什么是generator呢?
function* (){}
这就是一个匿名的generator。经过function关键字和函数名或者括号之间添加“*”定义一个generator函数,咱们也能够这样判断一个函数是否为generator:
typeof fn == 'function' && fn.constructor.name == 'GeneratorFunction'
在generator中咱们能够关键字yield,java程序员对yield确定不陌生,yield在java中是线程调度的一种方式,能够释放时间片让同级别的线程执行,然而在js中,yield却大不相同,由于js的执行线程是单线程,因此调度就不存在,yield咱们能够理解为函数执行的一个断点,每次只能执行到yield处,这样本来顺序或者异步执行的函数逻辑均可以经过某种方式使他们以顺序的方式呈如今咱们眼前,在这里须要强调下,经过yield只能断点执行generator函数中的逻辑,在函数以外并不会阻塞,不然整个主线程就会挂掉。
一个generator函数执行到yield处,咱们经过调用generator object的next()继续进行,generator object(下文简写为GO)就是generator函数的返回对象,调用GO的next方法会返回一个{value: '',done: false}这样的对象,value为yield关键字后面的表达式的值,done则表示generator函数是否执行完毕。
这就是基本的generator全部的数据结构,很简单明了。前端
function * fn(){ var a = yield 1; console.log(a); var b = yield 2; console.log(b); } var go = fn(); // 这是一个generator object go.next(); // 执行到第一个 yield ,执行表达式 1 go.next(); //执行到第二个yield,输出console.log(a)为undefined,执行表达式 2 go.next(); //执行console.log(b),输出 undefined
上面的demo很容易理解,可能惟一有点疑问的就是console.log的输出。这里强调,每次next,只执行yield后面的表达式,这样对于前面的赋值操做就无能为力,那么如何对a进行赋值呢?能够经过第二个next进行传值。经过对第二个go.next(2),这样a的值就被赋为2,同理b的值也能够这样传递。
可是,这对于异步流程控制有什么用呢?其实,仍是经过分段执行异步操做来完成。每一个yield async1()执行完毕,将结果做为参数传给下一个yield async2(),这样咱们只需判断GO.done是否为true来终止这个流程。java
咱们的目标是实现这种方式的流程控制:node
flow(function *(){ var readFile = helper(fs.readFile); var t1 = yield readFile('./files/f1', 'utf8'); var t2 = yield readFile(t1, 'utf8'); console.log(t2); });
其中flow是流程控制函数,参数为一个generator,helper函数则是一个包装函数,负责针对异步操做进行处理,下面咱们看看helper函数的逻辑。程序员
var helper = function(fn) { var feed; // 用于存储回调函数,该函数复用于全部用于helper处理的异步函数 /** * 执行次序分析: * helper的参数fn是一个异步函数,经过helper的处理,返回一个含有内部处理逻辑 * 的函数,该函数封装了所需参数和可能的回调函数feed,而且返回一个设置feed的函数。 * * 在具体的使用中,经过helper函数封装fs.readFile,获取readFile。当执行第一个 * 片断时,首先将全部的参数(包括feed)合并到args,并执行异步调用返回处理函数;此时 * 咱们用获取的返回函数设置回调函数,进而影响到args中的最后一项的函数 */ return function(){ var args = [].slice.call(arguments); args.push(function(){ if(feed) { feed.apply(null,arguments); } console.log(feed) }); fn.apply(null,args); // 返回一个函数,用于给yield以前的变量赋值 return function(fn){ feed = fn; } };
helper函数的做用就是从新包装异步函数,返回的包装函数也会返回一个函数,用于给回调函数feed赋值。
全部的异步函数都须要用helper进行封装,已传递必要的回调,最后按照flow分发的流程“依次执行”。
下面咱们实现flow的控制逻辑:chrome
var flow = function(gfn) { var generator = gfn(); next(); function next(data){ generator.ret = generator.next(data); if(generator.ret.done){ return; } generator.ret.value( function(error,d){ if(error) throw error; next.apply(null,[].slice.call(arguments,1)); } ); } };
逻辑依旧很简单,针对传入的generator生产generator object,最后进入next递归。在递归中,首先执行next逻辑并判断是否到了generator的终点,若是没有则调用generator object的value方法(此处为“被helper处理过得函数的返回值,即function(fn){feed = fn}”)对回调进行赋值,在回调中则递归执行next函数,直至generator结束逻辑。
经过这样的方式,咱们制定了flow流程,能够将多个异步操做顺序执行,而不影响generator函数以外的其他逻辑,这样避免了硬编码,没有了回调黑洞,咱们只需在异步函数前加yield便可,省时省事。express
flow(function *(){ var readFile = helper(fs.readFile); var nt = helper(process.nextTick); var t1 = yield readFile('./files/f1', 'utf8'); var t2 = yield readFile(t1, 'utf8'); yield nt(function(){console.log(t2)}); // console.log(t2); });
能够用helper封装各类异步回调,在具体的业务逻辑中传入其他回调返回值做为参数,从而达到目的。数组
yield 后面不只仅能够放置表达式,也能够放置数组。数组的每项为表达式,这样每次执行到yield时,会并行执行这些异步操做,返回对象的value属性也是一个数组,咱们依旧能够对value数组的每项进行赋值,从而完成回调的赋值。promise
var length = generator.ret.value.length, ret = []; generator.ret.value.forEach(function(item,i){ item(function(err,data) { --length; if (err) { console.log(err.message); // throw err; } ret.push(data); if(0 == length){ generator.next(ret); } }); });
对value值进行遍历,并判断并行的异步操做是否都已完成,若完成则传递ret数组给变量。数据结构
这块throw语法糖是后来添加的,之因此提到它是由于它的表现有点独特:
var gen = function* gen() { try { yield console.log('hello'); yield console.log('world'); } catch (e) { console.log(e); yield console.log('error...'); } yield console.log('end'); } var g = gen(); g.next(); g.throw('a'); g.next();
第一个next后,输出‘hello’;
throw后,输出‘a’、‘error...’
第二个next后,输出‘end’
能够发现gen.throw后,不只执行到catch代码块,并且还会执行下一个yield表达式,在这里须要注意下!
目前generator的兼容性要求其只能在node平台上使用,目前express框架的后继者koa采用了generator实现中间件的方式,中间件处理完每一个请求都会经过yield *next的方式进行分发,此处的next也是一个generator object,经过yield *next的方式能够嵌套多层generator链,这样next()就会到下一个generator的yield处。 分解函数的执行,这种方式确实让人耳目一新,咱们有理由相信js的将来,咱们要坚信js将来的能量,咱们要自豪咱们处在前端开发这个领域内。