第一次看koajs的示例时,发现该语句 function *(next){...............} ,这是啥啊?因而搜索一下,原来这是就是ES6的新特性Generator Function(生成器函数)。javascript
那什么是生成器函数呢?其实就至关于C#2.0中经过yield关键字实现的迭代器的生成器(细节有所不一样),那么理解的关键就在迭代器和yield关键字两部分了。下面将尝试从表象出发,逐步对生成器函数及利用它进行异步编程进行浅层的分析理解。html
示例:java
// 定义生成器函数 function *enumerable(msg){ console.log(msg) var msg1 = yield msg + ' after ' console.log(msg1) var msg2 = yield msg1 + ' after' try{ var msg3 = yield msg2 + 'after' console.log('ok') } catch(e){ console.log(e) } console.log(msg2 + ' over') } // 初始化迭代器 var enumerator = enumerable('hello') var ret = enumerator.next() // 控制台显示 hello,ret的值{value:'hello after',done:false} ret = enumerator.next('world') // 控制台显示 world,ret的值{value:'world after',done:false} ret = enumerator.next('game') // 控制台显示game,ret的值{value:'game after',done:false} // 抛出异常信息 ret = enumerator.throw(new Error('test')) // 控制台显示new Error('test')信息,而后显示game over。ret的值为{done:true} // for...of语句 enumerator = enumerable('hello') for(ret of enumerator) console.log(JSON.stringify(ret)); // 控制台依次显示 // hello // {value:'hello after',done:false} // world // {value:'world after',done:false} // {value:'game after',done:false} // game over // {done:true}
function* test(){} function * test(){} function *test(){} test = function* (){} test = function *(){} 普通函数添加*号后则成为了成为了生成器函数了。 Object.prototype.toString.call(test) // 显示[object GeneratorFunction] 生成器函数的行为与普通函数并不相同,表现为以下3点: 1. 经过new运算符或函数调用的形式调用生成器函数,均会返回一个生成器实例; 2. 经过new运算符或函数调用的形式调用生成器函数,均不会立刻执行函数体的代码; 3. 必须调用生成器实例的next方法才会执行生成器函数体的代码。 function *say(msg){ console.log(msg) } var gen = say('hello world') // 没有显示hello world console.log(Object.prototype.toString.call(gen)) // 显示[object Generator] gen.next() // 显示hello world
用于立刻退出代码块并保留现场,当执行迭代器的next函数时,则能从退出点恢复现场并继续执行下去。下面有2点须要注意:
1. yield后面的表达式将做为迭代器next函数的返回值;
2. 迭代器next函数的入参将做为yield的返回值(有点像运算符)。
三、迭代器(Generator)
迭代器是一个拥有 {value:{}, done:{Boolean}} next([])方法 和 {undefined} throw([*])方法 的对象,经过next函数不断执行以关键字yield分割的代码段,经过throw函数令yield分割的代码段抛出异常。git
迭代器更多的是指迭代器模式,迭代器模式是指经过一个名为迭代器的对象按必定的规则遍历集合元素,调用者只需告诉迭代器获取下一个元素便可,而集合的类型、如何获取元素等因素均由具体的迭代器自行处理。(又一次地关注点分离!)而且因为迭代器模式能够作到 按需执行/延迟执行 的效果,所以能下降遍历无限序列时内存/栈溢出的问题,也能做为异步编程模式使用。
模式理解的注意点:
1. 迭代器每次进访问集合的一个元素,并由调用者发起访问请求时迭代器才执行下一次访问操做
2. “按必定的规则”,意味着不必定遍历集合中全部的元素,而且规则能够内聚到迭代器的具体实现上,也可经过策略模式外移到其余模块中;
3. “集合”,集合能够是一开始就已经初始化好的有限序列集合(如[1,2,3,4,5,6,7]),也能够是按需生成的无限序列集合(如1到无限大)
4. “集合元素”,能够是整数集合、字符串集合等数据集合,也能够是函数等指令+数据的集合;
若触过C#、Java等服务端语句的朋友应该对迭代器有必定程度的了解,C#的IEnumrable、IEnumerator和Java的Iterable、Iterator就是跟迭代器相关的接口定义,继承上述接口的迭代器实现都可以经过foreach或for...in语句做循环操做。es6
那么这里有2点是要注意的:
1. 迭代器是指设计模式,跟具体的语言无关,所以全部语言都可根据该模式实现具体的迭代器;
2. foreach或for...in语句是语法层面的支持,跟迭代器模式没有必然联系。(若语法层面不支持,那函数式编程中的递归的效果是同样的,假如编译器/解析器支持尾递归则更好了,能够JS不支持)
下面咱们经过迭代器来实现Python中的range函数,并经过range函数建立一个超大的有限序列正整数集合(直接用数组的话绝有可能致使栈溢出哦!)。github
// 迭代器构造函数 var RangeIterator = function(start,end,scan){ this.start = arguments.length >= 2 ? start : 0 this.end = end == undefined ? start : end this.scan = scan || 1 this.idx = this.start } // 向迭代器发起访问下一个元素的请求 // FF和ES6下迭代器接口规范定义了迭代器必须经过名为next的函数发起访问下一个元素的请求 RangeIterator.prototype.next = function(){ if (this.idx > this.end) if (!!StopIteration) { throw StopIteration }else{ return void 0 } var ret = this.idx this.idx += this.scan return ret } // Python中的range函数 var range = function(start, end, scan){ var iterator = new RangeIterator(start, end, scan) return { // FF下令for...in语句调用对象的迭代器的接口规范 __iterator__: function(){ return iterator }, // 暴露迭代器的next函数 next: function(){ return iterator.next() }, toString: function(){ // 可能会致使栈溢出 var array = [] for (var i = this.next(); i != void 0; i = this.next()) array.push(i) return array + '' } } } var r = range(1, 100000000000000000000) // FF下 // 参考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Iterators_and_Generators#.E5.AE.9A.E4.B9.89.E8.87.AA.E5.AE.9A.E4.B9.89.E8.BF.AD.E4.BB.A3.E5.99.A8 for(var i in r) console.log(i) // 显示1到99999999999999999999 // 全部浏览器 for (var i = r.next(); i != void 0; i = r.next()) console.log(i) // 显示1到99999999999999999999
因为JS是单线程运行,而且当UI线程被阻塞N秒后,浏览器会询问是否中止脚本的执行,但上述代码并不会因为序列过大形成栈溢出的问题。假如预先生成1到99999999999999999999或更大数字的数组,那颇有可能形成stack overflow。那是因为迭代器实质为一状态机,而调用next函数则是触发状态的转换,而状态机中同一时刻用于存放变量的存储空间固定,并不会出现无限增加的状况。编程
回到关键字yield上了,其实yield关键字就是以一种更直观、便捷的方式让咱们建立用于遍历有限序列集合的迭代器,而yield则用于将生成器函数的代码切片做为有限序列集合的元素(元素的类型为指令+数据,而不只仅是数据而已)。下面咱们一块儿看看yield关键字是怎样对代码切片的吧!json
// 定义生成器函数 function *enumerable(msg){ console.log(msg) var msg1 = yield msg + ' after ' console.log(msg1) var msg2 = yield msg1 + ' after' console.log(msg2 + ' over') }
上述代码最终会被解析为下面的代码:设计模式
var enumerable = function(msg){ var state = -1 return { next: function(val){ switch(++state){ case 0: console.log(msg + ' after') break case 1: var msg1 = val console.log(msg1 + ' after') break case 2: var msg2 = val console.log(msg2 + ' over') break } } } }
(注意:上述仅仅简单的分析,更复杂的状况(条件控制、循环、迭代、异常捕获处理等)能够参考@赵劼的《人肉反编译使用关键字yield的方法》)数组
因为迭代器模式实现 延迟执行/按需执行,所以可做为一种异步编程模式来应用。
var iterator = getArticles('dummy.json') // 开始执行 iterator.next() // 异步任务模型 function getData(src){ setTimeout(function(){ iterator.next({tpl: 'tpl.html', name: 'fsjohnhuang'}) }, 1000) } function getTpl(tpl){ setTimeout(function(){ iterator.next('hello ${name}') }, 3000) } // 同步任务 function render(data, tpl){ return tpl.replace(/\$\{(\w+)\}/, function(){ return data[arguments[1]] == void 0 ? arguments[0] : data[arguments[1]] }) } // 主逻辑 function *getAritcles(src){ console.log('begin') var data = yield getData(src) var tpl = yield getTpl(data.tpl) var res = render(data, tpl) console.log(rest) }
主逻辑中异步调用的写法与同步调用的基本没差别了,爽了吧!但异步任务模型与生成器函数及其生成的迭代器耦合性太大,仍是不太好用。下面咱们经过实现了Promises/A+规范的Q来进一步解耦。
若执行引擎不支持关键字yield,那么上述代码不就没法执行了吗?仍是那句话,yield关键字其实就是语法糖,最终仍是会被解析为一个迭代器。所以咱们自行实现一个迭代器也是能实现上述效果的,不过过程会繁琐不少(若如第2节的示例那样存在try...catch语句,就繁琐死了@~@),而且代码的整洁性、可维护性就全靠攻城狮来保证了。(语法糖从语法层面简化编程和维护难度,但理解底层的工做原理也十分重要哦!)
// 异步任务模型 function getData(src){ var deferred = Q.defer() setTimeout(function(){ defer.resolve({tpl: 'tpl.html', name: 'fsjohnhuang'}) }, 1000) return deferred.promise } function getTpl(tpl){ var deferred = Q.defer() setTimeout(function(){ defer.resolve('hello ${name}') }, 3000) return deferred.promise } // 同步任务 function render(data, tpl){ return tpl.replace(/\$\{(\w+)\}/, function(){ return data[arguments[1]] == void 0 ? arguments[0] : data[arguments[1]] }) } // 主逻辑 Q.async(function *(){ console.log('begin') var data = yield getData('dummy.json') var tpl = yield getTpl(data.tpl) var res = render(data, tpl) console.log(rest) })
暂未阅读Q的源代码,暂不做详细分析。反正API就这样用,呵呵!
iPromise是我开发的一个Promises/A+的完整实现,阅读源码你会发现它继承了jQuery.Deferred1.5~2.一、jsDeferred、mmDeferred和Promises/A官网实现示例的精妙设计,而且从v0.0.6开始支持ES6特性GeneratorFunction。使用示例以下:
var getData = function(dataSrc){ return iPromise(function(r){ setTimeout(function(){ r(dataSrc + ' has loaded') }, 1000) }) } var getTpl = function(tplSrc){ return iPromise(function(r){ setTimeout(function(){ r(tplStr + ' has loaded') }, 2000) }) } var render = function(data, tpl){ throw new Error('OMG!') } iPromise(function *(dataSrc, tplSrc){ try{ var data = yield getData(dataSrc) var tpl = yield getTpl(tplSrc) render(data, tpl) } catch(e){ console.log(e) } console.log('over!') }, 'dummyData.json', 'dummyTpl.json') /* 结果以下 */ // 等待1秒多显示 dummyData.json has loaded // 等待2秒多显示 dummyTpl.json has loaded // 显示 Error: OMG! // Stack trace: // test10/render/</<@file:///home/fsjohnhuang/repos/iPromise/test/v0.0.2.html:190:6 // 显示 over!
v0.6.0的中经过递归来实现,具体以下(https://github.com/fsjohnhuang/iPromise/blob/master/src/iPromise.js#L7...):
// FF下生成器函数的入参必须在建立迭代器时传递 // 若第一次调用迭代器的next函数传递参数,则会报TypeError: attempt to send 第一个入参值 to newborn generator var iterator = mixin.apply(null, toArray(arguments,1)) var next = function(){ var deferred = iPromise() deferred.resolve.apply(deferred, arguments) return deferred.then(function(){ var yieldReturn = iterator.next.apply(iterator, arguments) if(yieldReturn.done) throw Error('StopIteration') return yieldReturn.value }).then(next, function(e){ iterator.throw(e) }) } deferred.resolve() deferred.then(next)
Generator Function并非为异步编程而生,但能够将它结合Promise来实现良好的异步编程模型。本篇内容仅简单介绍Generator Function及相关的异步编程内容,如有纰漏请各位指正,谢谢!
http://huangj.in/765
https://www.imququ.com/post/generator-function-in-es6.html
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/The_Iter...
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Stat...*
http://www.cnblogs.com/yangecnu/archive/2012/03/17/2402432.html
http://www.cnblogs.com/draem0507/p/3795189.html
http://blog.zhaojie.me/2010/06/code-for-fun-iterator-generator-yield-i...
http://blog.zhaojie.me/2010/06/code-for-fun-iterator-generator-yield-i...
http://blog.zhaojie.me/2010/07/why-java-sucks-and-csharp-rocks-6-yield... 若是您以为本文的内容有趣就扫一下吧!捐赠互勉!