原文: 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變換的細節,會在以後的文章中詳細解說。