【轉向Javascript系列】深刻理解Generators

隨着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()函數稱之爲生成器函數,與普通函數相比兩者有以下區別閉包

  • 普通函數使用function聲明,生成器函數用function*聲明
  • 普通函數使用return返回值,生成器函數使用yield返回值
  • 普通函數是run to completion模式,即普通函數開始執行後,會一直執行到該函數全部語句完成,在此期間別的代碼語句是不會被執行的;生成器函數是run-pause-run模式,即生成器函數能夠在函數運行中被暫停一次或屢次,而且在後面再恢復執行,在暫停期間容許其餘代碼語句被執行

對於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傳遞給它自身和其引用者,每個迭代器保存一個狀態指示

  • -2:初始化爲可迭代類Enumerable
  • -1:迭代結束
  • 0:初始化爲迭代器Enumerator
  • 1-n:原始Example()方法中的yield return索引值

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關鍵字的特性。

迭代器狀態機模型可以下圖所示

  • Before爲迭代器初始狀態
  • Running 爲調用MoveNext後進入這個狀態。在這個狀態,枚舉數檢測並設置下一項的位置。遇到yield return、yield break或者迭代結束時,退出該狀態
  • Suspended爲狀態機等待下次調用MoveNext的狀態
  • After爲迭代結束的狀態

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

相關文章
相關標籤/搜索