原文: http://pij.robinqu.me/JavaScript_Core/Functional_JavaScript/JavaScript_Generator.htmlhtml
ES6中的Generator的引入,极大程度上改变了Javascript程序员对迭代器的见解,并为解决callback hell1提供了新方法。git
Generator是一个与语言无关的特性,理论上它应该存在于全部Javascript引擎内,可是目前真正完整实现的,只有在node --harmony
下。因此后文全部的解释,都以node环境举例,须要的启动参数为node --harmony --use_strict
。程序员
V8中所实现的Generator和标准之中说的又有区别,这个能够参考一下MDC的相关文档2。并且,V8在写做这篇文章时,并无实现Iterator。es6
咱们以一个简单的例子3开始:github
function* argumentsGenerator() { for (let i = 0; i < arguments.length; i += 1) { yield arguments[i]; } }
咱们但愿迭代传入的每一个实参:数组
var argumentsIterator = argumentsGenerator('a', 'b', 'c'); // Prints "a b c" console.log( argumentsIterator.next().value, argumentsIterator.next().value, argumentsIterator.next().value );
咱们能够简单的理解:app
argumentsGenerator
被称为GeneartorFunction
,也有些人把GeneartorFunction
的返回值称为一个Geneartor
。yield
能够中断GeneartorFunction
的运行;而在下一次yield
时,能够恢复运行。Iterator
上,有next
成员方法,可以返回迭代值。其中value
属性包含实际返回的数值,done
属性为布尔值,标记迭代器是否完成迭代。要注意的是,在done
属性为true
后继续运行next
方法会产生异常。完整的ES实现中,for-of
循环正是为了快速迭代一个iterator
的:异步
// Prints "a", "b", "c" for(let value of argumentsIterator) { console.log(value); }
惋惜,目前版本的node不支持for-of
。async
说到这里,大多数有经验的Javascript程序员会表示不屑,由于这些均可以经过本身编写一个函数来实现。咱们再来看一个例子:
function* fibonacci() { let a = 0, b = 1; //1, 2 while(true) { yield a; a = b; b = a + b; } } for(let value of fibonacci()) { console.log(value); }
fibonacci序列是无穷的数字序列,你能够用函数的迭代来生成,可是远没有用Generator来的简洁。
再来个更有趣的。咱们能够利用yield*
语法,将yield操做代理到另一个Generator
。
let delegatedIterator = (function* () { yield 'Hello!'; yield 'Bye!'; }()); let delegatingIterator = (function* () { yield 'Greetings!'; yield* delegatedIterator; yield 'Ok, bye.'; }()); // Prints "Greetings!", "Hello!", "Bye!", "Ok, bye." for(let value of delegatingIterator) { console.log(value); }
yield
能够暂停运行流程,那么便为改变执行流程提供了可能4。这和Python的coroutine相似。
co已经将此特性封装的很是完美了。咱们在这里简单的讨论其实现。
The classic example of this is consumer-producer relationships: generators that produce values, and then consumers that use them. The two generators are said to be symmetric – a continuous evaluation where coroutines yield to each other, rather than two functions that call each other.
Geneartor之因此可用来控制代码流程,就是经过yield来将两个或者多个Geneartor的执行路径互相切换。这种切换是语句级别的,而不是函数调用级别的。其本质是CPS变幻,后文会给出解释。
这里要补充yield的若干行为:
GeneratorFunction
的return语句等同于一个yield假设咱们但愿有以下语法风格:
GeneratorFunction
yield
,看起来像同步调用GeneratorFunction
的返回值和执行过程的错误都会会传入全局的回调函数更具体的,以下例子:
var fs = require("fs"); suspend(function*(resume) { var content = yield fs.readFile(__filename, resume); var list = yield fs.readdir(__dirname, resume); return [content, list]; })(function(e, res) { console.log(e,res); });
上面分别进行了一个读文件和列目录的操做,均是异步操做。为了实现这样的suspend
和resume
。咱们简单的封装Generator的API:
var slice = Array.prototype.slice.call.bind(Array.prototype.slice); var suspend = function(gen) {//`gen` is a generator function return function(callback) { var args, iterator, next, ctx, done; ctx = this; args = slice(arguments); next = function(e) { if(e) {//throw up or send to callback return callback ? callback(e) : iterator.throw(e); } var ret = iterator.next(slice(arguments, 1)); if(ret.done && callback) {//run callback is needed callback(null, ret.value); } }; resume = function(e) { next.apply(ctx, arguments); }; args.unshift(resume); iterator = gen.apply(this, args); next();//kickoff }; };
目前咱们只支持回调形势的API,而且须要显示的传入resume
做为API的回调。为了像co
那样支持更多的能够做为yield
参数。co
中,做者将全部形势的异步对象都归结为一种名为thunk
的回调形式。
那什么是thunk
呢?thunk
就是支持标准的node风格回调的一个函数: fn(callback)
。
首先咱们将suspend修改成自动resume:
var slice = Array.prototype.slice.call.bind(Array.prototype.slice); var suspend = function(gen) { return function(callback) { var args, iterator, next, ctx, done; ctx = this; args = slice(arguments); next = function(e) { if(e) { return callback ? callback(e) : iterator.throw(e); } var ret = iterator.next(slice(arguments, 1)); if(ret.done && callback) { return callback(null, ret.value); } if("function" === typeof ret.value) {//shold yield a thunk ret.value.call(ctx, function() {//resume function next.apply(ctx, arguments); }); } }; iterator = gen.apply(this, args); next(); }; };
注意,这个时候,咱们只能yield
一个thunk
,咱们的使用方法也要发生改变:
var fs = require("fs"); read = function(filename) {//wrap native API to a thunk return function(callback) { fs.readFile(filename, callback); }; }; suspend(function*() {//return value of this generator function is passed to callback return yield read(__filename); })(function(e, res) { console.log(e,res); });
接下来,咱们要让这个suspend更加有用,咱们能够支持以下内容穿入到yield
var slice = Array.prototype.slice.call.bind(Array.prototype.slice); var isGeneratorFunction = function(obj) { return obj && obj.constructor && "GeneratorFunction" == obj.constructor.name; }; var isGenerator = function(obj) { return obj && "function" == typeof obj.next && "function" == typeof obj.throw; }; var suspend = function(gen) { return function(callback) { var args, iterator, next, ctx, done, thunk; ctx = this; args = slice(arguments); next = function(e) { if(e) { return callback ? callback(e) : iterator.throw(e); } var ret = iterator.next(slice(arguments, 1)); if(ret.done && callback) { return callback(null, ret.value); } if(isGeneratorFunction(ret.value)) {//check if it's a generator thunk = suspend(ret.value); } else if("function" === typeof ret.value) {//shold yield a thunk thunk = ret.value; } else if(isGenerator(ret.value)) { thunk = suspend(ret.value); } thunk.call(ctx, function() {//resume function next.apply(ctx, arguments); }); }; if(isGeneratorFunction(gen)) { iterator = gen.apply(this, args); } else {//assume it's a iterator iterator = gen; } next(); }; };
在使用时,咱们能够传入三种对象到yield:
var fs = require("fs"); read = function(filename) { return function(callback) { fs.readFile(filename, callback); }; }; var read1 = function*() { return yield read(__filename); }; var read2 = function*() { return yield read(__filename); }; suspend(function*() { var one = yield read1; var two = yield read2(); var three = yield read(__filename); return [one, two, three]; })(function(e, res) { console.log(e,res); });
固然,到这里,你们应该都明白如何让suspend
兼容更多的数据类型,例如Promise
、数组等。但更多的扩展,在这里就再也不赘述。这里的suspend
能够就说就是精简的co
了。
yield
的引入,让流程控制走上了一条康庄大道,不须要使用复杂的Promise
、也不用使用难看的async
。同时,从性能角度,yield能够经过V8的后续优化,性能进一步提高,目前来讲yield
的性能并不差5。
yield
的本质是一个语法糖,底层的实现方式即是CPS变换6。也就是说yield
是能够用循环和递归从新实现的,根本用不着必定在V8层面实现。但笔者认为,纯Javascript实现的"yield"会形成大量的堆栈消耗,在性能上毫无优点可言。从性能上考虑,V8能够优化yield
的编译,实现更高性能的转换。
关于CPS变换的细节,会在以后的文章中详细解说。