隨着Javascript語言的發展,ES6規範爲咱們帶來了許多新的內容,其中生成器Generators是一項重要的特性。利用這一特性,咱們能夠簡化迭代器的建立,更加使人興奮的,是Generators容許咱們在函數執行過程當中暫停、並在未來某一時刻恢復執行。這一特性改變了以往函數必須執行完成才返回的特色,將這一特性應用到異步代碼編寫中,能夠有效的簡化異步方法的寫法,同時避免陷入回調地獄。html
本文將對Generators進行簡單介紹,而後結合筆者在C#上的一點經驗,重點探討Generators運行機制及在ES5的實現原理。es6
1.Generators簡單介紹編程
一個簡單的Generator函數示例babel
function* example() { yield 1; yield 2; yield 3; } var iter=example(); iter.next();//{value:1,done:false} iter.next();//{value:2,done:false} iter.next();//{value:3,done:false} iter.next();//{value:undefined,done:true}
上述代碼中定義了一個生成器函數,當調用生成器函數example()時,並不是當即執行該函數,而是返回一個生成器對象。每當調用生成器對象的.next()方法時,函數將運行到下一個yield表達式,返回表達式結果並暫停自身。當抵達生成器函數的末尾時,返回結果中done的值爲true,value的值爲undefined。咱們將上述example()函數稱之爲生成器函數,與普通函數相比兩者有以下區別閉包
對於Generators的使用,本文再也不多作介紹,如需瞭解更多內容推薦閱讀下面系列文章,《ES6 Generators: Complete Series》或者《深刻掌握 ECMAScript 6 異步編程》系列文章併發
2.Generators in C#異步
生成器不是一個新的概念,我最初接觸這一律念是在學習使用C#時。C#從2.0版本便引入了yield關鍵字,使得咱們能夠更簡單的建立枚舉數和可枚舉類型。不一樣的是C#中未將其命名爲生成器Generators,而將其稱之爲迭代器。ide
本文不會介紹C#中可枚舉類IEnumerable和枚舉數IEnumerator內容,如需瞭解推薦閱讀《C#4.0圖解教程》相關章節。異步編程
2.1 C#迭代器介紹函數
讓咱們先看一個示例,下面方法聲明實現了一個產生和返回枚舉數的迭代器
public IEnumerable <int> Example() { yield return 1; yield return 2; yield return 3; }
方法定義與ES6 Generators定義很接近,定義中聲明返回了一個int類型的泛型可枚舉類型,方法體內經過yield return語句返回值並將自身暫停執行。
使用迭代器來建立可枚舉類型的類
class YieldClass { public IEnumerable<int> Example()//迭代器 { yield return 1; yield return 2; yield return 3; } } class Program { static void Main() { YieldClass yc=new YieldClass (); foreach(var a in yc.Example()) Console.WriteLine(a); } }
上述代碼會產生以下輸入
1 2 3
2.2 C#迭代器原理
在.Net中,yield並非.Net runtime的特性,而是一個語法糖,代碼編譯時,這一語法糖會被C#編譯器編譯成簡單的IL代碼。
繼續研究上述示例,經過Reflector反編譯工具能夠看到,編譯器爲咱們生成了一個帶有以下聲明的內部類
[CompilerGenerated] private sealed class YieldEnumerator : IEnumerable<object>, IEnumerator<object> { // Fields字段 private int state; private int current; public YieldClass owner; private int initialThreadId; // Methods方法 [DebuggerHidden] public YieldEnumerator(int state); private bool MoveNext(); [DebuggerHidden] IEnumerator<int> IEnumerable<int>.GetEnumerator(); [DebuggerHidden] IEnumerator IEnumerable.GetEnumerator(); [DebuggerHidden] void IEnumerator.Reset(); void IDisposable.Dispose(); // Properties屬性 object IEnumerator<object>.Current { [DebuggerHidden] get; } object IEnumerator.Current { [DebuggerHidden] get; } }
原始的Example()方法僅返回一個YieldEnumerator的實例,並將初始狀態-2傳遞給它自身和其引用者,每個迭代器保存一個狀態指示
Example()方法中代碼被轉換爲YieldingEnumerator.MoveNext(),在咱們的示例中轉換後代碼以下
bool MoveNext() { switch (state) { case 0: state = -1; current = 1; state = 1; return true; case 1: state = -1; current = 2; state = 2; return true; case 2: state = -1; current = 3; state = 3; return true; case 3: state = -1; break; } return false; }
利用上述的代碼轉換,編譯器爲咱們生成了一個狀態機,正是基於這一狀態機模型,實現了yield關鍵字的特性。
迭代器狀態機模型可以下圖所示
3.Generators in Javascript
經過閱讀上文,咱們瞭解了Generator在C#中的使用,而且經過查看編譯器生成的IL代碼,得知編譯器會生成一個內部類來保存上下文信息,而後將yield return表達式轉換成switch case,經過狀態機模式實現yield關鍵字的特性。
3.1 Javascript Generators原理淺析
yield關鍵字在Javascript中如何實現呢?
首先,生成器不是線程。支持線程的語言中,多段不一樣的代碼能夠在同一時候運行,這常常會致使資源競爭,使用得當會有不錯的性能提高。生成器則徹底不一樣,Javascript執行引擎仍然是一個基於事件循環的單線程環境,當生成器運行的時候,它會在叫作 caller 的同一個線程中運行。執行的順序是有序、肯定的,而且永遠不會產生併發。不一樣於系統的線程,生成器只會在其內部用到 yield 的時候纔會被掛起。
既然生成器並不是由引擎從底層提供額外的支持,咱們能夠沿用上文在C#中對yield特性的原理探究的經驗,將生成器視爲一個語法糖,用一個輔助工具將生成器函數轉換爲普通的Javascript代碼,在通過轉換的代碼中,有兩個關鍵點,一是要保存函數的上下文信息,二是實現一個完善的迭代方法,使得多個yield表達式按序執行,從而實現生成器的特性。
3.2 How Generators work in ES5
Regenerator工具已經實現了上述思路,藉助Regenerator工具,咱們已經能夠在原生ES5中使用生成器函數,本節咱們來分析Regenerator實現方式以深刻理解Generators運行原理。
經過這個在線地址能夠方便的查看通過轉換後的代碼,仍然以文章初始爲例
function* example() { yield 1; yield 2; yield 3; } var iter=example(); iter.next();
通過轉換後爲
var marked0$0 = [example].map(regeneratorRuntime.mark); function example() { return regeneratorRuntime.wrap(function example$(context$1$0) { while (1) switch (context$1$0.prev = context$1$0.next) { case 0: context$1$0.next = 2; return 1; case 2: context$1$0.next = 4; return 2; case 4: context$1$0.next = 6; return 3; case 6: case "end": return context$1$0.stop(); } }, marked0$0[0], this); } var iter = example(); iter.next();
從轉換後的代碼中能夠看到,與C#編譯器對yield return表達式的轉換類似,Regenerator將生成器函數中的yield表達式重寫爲switch case,同時,在每一個case中使用context$1$0來保存函數當前的上下文狀態。
switch case以外,迭代器函數example被regeneratorRuntime.mark包裝,返回一個被regeneratorRuntime.wrap包裝的迭代器對象。
runtime.mark = function(genFun) { if (Object.setPrototypeOf) { Object.setPrototypeOf(genFun, GeneratorFunctionPrototype); } else { genFun.__proto__ = GeneratorFunctionPrototype; } genFun.prototype = Object.create(Gp); return genFun; };
經過mark包裝,將example包裝成以下對象
當調用生成器函數example()時,返回一個被wrap函數包裝後的迭代器對象
runtime.wrap=function (innerFn, outerFn, self, tryLocsList) { // If outerFn provided, then outerFn.prototype instanceof Generator. var generator = Object.create((outerFn || Generator).prototype); var context = new Context(tryLocsList || []); // The ._invoke method unifies the implementations of the .next, // .throw, and .return methods. generator._invoke = makeInvokeMethod(innerFn, self, context); return generator; }
返回的迭代器對象以下所示
當調用迭代器對象iter.next()方法時,由於有以下代碼,因此會執行_invoke方法,而根據前面wrap方法代碼可知,最終是調用了迭代器對象的makeInvokeMethod (innerFn, self, context);方法
// Helper for defining the .next, .throw, and .return methods of the // Iterator interface in terms of a single ._invoke method. function defineIteratorMethods(prototype) { ["next", "throw", "return"].forEach(function(method) { prototype[method] = function(arg) { return this._invoke(method, arg); }; }); }
makeInvokeMethod方法內容較多,這裏選取部分分析。首先,咱們發現生成器將自身狀態初始化爲「Suspended Start」
function makeInvokeMethod(innerFn, self, context) { var state = GenStateSuspendedStart; return function invoke(method, arg) {
makeInvokeMethod返回invoke函數,當咱們執行.next方法時,實際調用的是invoke方法中的下面語句
var record = tryCatch(innerFn, self, context);
這裏tryCatch方法中fn爲通過轉換後的example$方法,arg爲上下文對象context,由於invoke函數內部對context的引用造成閉包引用,因此context上下文得以在迭代期間一直保持。
function tryCatch(fn, obj, arg) { try { return { type: "normal", arg: fn.call(obj, arg) }; } catch (err) { return { type: "throw", arg: err }; } }
tryCatch方法會實際調用example$方法,進入轉換後的switch case,執行代碼邏輯。若是獲得的結果是一個普通類型的值,咱們將它包裝成一個可迭代對象格式,而且更新生成器狀態至GenStateCompleted或者GenStateSuspendedYield
var record = tryCatch(innerFn, self, context); if (record.type === "normal") { // If an exception is thrown from innerFn, we leave state === // GenStateExecuting and loop back for another invocation. state = context.done ? GenStateCompleted : GenStateSuspendedYield; var info = { value: record.arg, done: context.done };
4.總結
經過對Regenerator轉換後的生成器代碼及工具源碼分析,咱們探究了生成器的運行原理。Regenerator經過工具函數將生成器函數包裝,爲其添加如next/return等方法。同時也對返回的生成器對象進行包裝,使得對next等方法的調用,最終進入由switch case組成的狀態機模型中。除此以外,利用閉包技巧,保存生成器函數上下文信息。
上述過程與C#中yield關鍵字的實現原理基本一致,都採用了編譯轉換思路,運用狀態機模型,同時保存函數上下文信息,最終實現了新的yield關鍵字帶來的新的語言特性。
參考文章
1.ES6 Generators:Complete Series系列文章
2.深刻淺出ES6 Generators
3.《深刻掌握 ECMAScript 6 異步編程》系列文章
4.ES6 Generators:How do they work?
5.Behind the scenes of the C# yield keyword