一、前言
第一次看koajs的示例时,发现该语句 function *(next){……………} ,这是啥啊?于是搜索一下,原来这是就是ES6的新特性Generator Function(生成器函数)。
那什么是生成器函数呢?其实就相当于C#2.0中通过yield关键字实现的迭代器生成器(细节有所不同),那么理解的关键就在yield关键字了。下面将尝试从表象出发,逐步对生成器函数及利用它进行异步编程进行浅层的分析理解。
二、语法及基本使用
示例:
// 定义生成器函数 function *enumerable(msg){ console.log(msg) var msg1 = yield msg + ' after ' console.log(msg1) var msg2 = yield msg1 + ' after' 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 over,ret的值{done:true} // for...of语句 enumerator = enumerable('hello') for(ret of enumerator) console.log(JSON.stringify(ret)); // 控制台一次显示 // {value:'hello after',done:false} // {value:'world after',done:false} // {done:true}
1. 生成器语函数定义
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
2、 关键字yield——迭代器生成器
用于马上退出代码块并保留现场,当执行迭代器的next函数时,则能从退出点恢复现场并继续执行下去。下面有2点需要注意:
1. yield后面的表达式将作为迭代器next函数的返回值;
2. 迭代器next函数的入参将作为yield的返回值(有点像运算符)。
3、迭代器(Generator)
迭代器是一个拥有 {value:{*}, done:{Boolean}} next([*])方法 的对象,通过next函数不断执行以关键字yield分割的代码段
三、迭代器
迭代器是迭代器模式的实现,好处是“延迟执行”,通过迭代器我们可以很轻松地实现Python中的rang函数来创建一个庞大的序列。
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 } 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 } var range = function(start, end, scan){ var iterator = new RangeIterator(start, end, scan) return { __iterator__: function(){ return iterator }, 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下 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则用于分割迭代器不同状态所执行的代码段。下面我们一起简单反编译yield关键字。
// 定义生成器函数 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来进一步解耦。
六、与Q结合
// 异步任务模型 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) })