【Javascriptシリーズへの移行】Generatorsを深く理解する

20760 ワード

Javascript言語の発展に伴い、ES 6仕様は多くの新しい内容をもたらしてくれます.その中でジェネレータGeneratorsは重要な特性です.この特性を利用して、私たちは、よりエキサイティングなのは、ジェネナートの作成を簡単にすることができます.私たちは関数の実行中に一時停止し、将来のある時間に実行を再開することができます.この特性は、従来の関数が実行されてから戻ってくるという特徴を変えています.この特性を非同期コードの編纂に応用すれば、非同期方法の書き方を効果的に簡略化できます.
本論文ではGeneratorsについて簡単に紹介し、筆者のC嗳上の経験を結びつけて、Generators運行メカニズムとES 5での実現原理を重点的に検討する.
 
1.Generators簡単な紹介
簡単なGenerator関数の例
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 complectionモードであり、一般関数が実行を開始した後、その関数のすべての語句が完了するまで実行されます.この期間に他のコードステートメントは実行されません.ジェネレータ関数は、run-pause-runモードであり、すなわち、ジェネレータ関数は、関数運転において一度または複数回停止され、その後に実行が再開され、一時停止中に他のコードステートメントが実行されることができる
  • .
    Generatorsの使用については、ここでは多くのことを紹介しません.もっと多くのことを知るなら、次のシリーズの文章を読むことを勧めます.「ES 6 Generators:Coplete Series」または「ECMAScript 6非同期プログラミングを深く把握する」シリーズの文章を読みます.
    2.Generators in C铉
    生成器は新しい概念ではなく、私が最初にこの概念に接触したのはCを使う時です.C((zhi)は2.0バージョンからyieldのキーワードを導入しています.列挙数と列挙可能なタイプを簡単に作成できます.違っているのはC〓〓の中でそれをジェネレータGeneratorsと命名していないので、それをローズマリーと呼びます.
    本論文では、C铉中のエニュメレーション類IEnumerableとエニュメレーション数IEnumeratorの内容は紹介されません.例えば、「C顫4.0図解教程」関連の章を読むことをお勧めします.
    2.1 C嗳重ね着器の紹介
    まず一例を見てみましょう.次の方法は、列挙の数を生成し、戻すためのディケンサを実現しています.
     
    public IEnumerable <int> Example()
    {
            yield return 1;
            yield return 2;
            yield return 3;
    }
     
    方法定義はES 6 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()方法は、Yield Enumeratorの例だけを返し、初期状態-2を自身とその引用者に伝え、各サブエージェントは状態表示を保存します.
  • -2:イネーブルエムラール
  • に初期化する.
  • -1:反復終了
  • :ローズマリーに初期化されました.
  • 1−n:元のExample()方法におけるyield returnインデックス値
  • Example()メソッドのコードは、YieldingEumerator.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((zhi)における使用を理解し、コンパイラで生成されたILコードを見ることによって、コンパイラが内部クラスを生成してコンテキスト情報を保存すると分かりました.その後、yield return式をswitch caseに変換して、状態マシンモードでyieldキーワードの特性を実現します.
    3.1 Javascript Generatorsの原理は簡単に分析します.
    yieldのキーワードはJavascriptの中でどうやって実現されますか?
    まず、生成器はスレッドではない.スレッドをサポートする言語では、複数の異なるコードが同じ時に実行されます.これはしばしば資源競争を招き、適切に使用すると良い性能が向上します.生成器は全く違っています.Javascript実行エンジンはイベントサイクルに基づく単一スレッド環境であり、生成器が動作すると、calerという同じスレッドで動作します.実行の順序は整然としていて、確定しています.そして永遠に合併が発生しません.システムのスレッドとは違って、ジェネレータは内部でyieldを使用した時だけマウントされます.
    ジェネレータは、エンジンによって下から追加のサポートを提供していないので、C((xi)においてyield特性の原理を探究した上での経験を生かして、ジェネレータをシンタックスキャンディーと見なし、ジェネレータ関数を1つの補助ツールで普通のJavascriptコードに変換します.変換されたコードの中には、2つの鍵があります.1つは、関数のコンテキスト情報を保存します.二つ目は、複数のyield表現が順序に従って実行されるように、完全な反復方法を実現することであり、それによって生成器の特性が実現される.
    3.2 How Generators work in ES 5
    レゲナートツールはすでに上述の構想を実現しました.レゲナートツールを利用して、私達はすでに元のES 5にジェネレータ関数を使用できます.この節はレゲナートの実現方式を分析して、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((zhi)コンパイラのyield return表現への変換と同様に、Regeneratorがジェネレータ関数のyield表現をswitch caseと書き換え、各caseでcontext$1ドル0を使って関数の現在のコンテキスト状態を保存することができます.
    switch caseのほかに、ディレクタ関数exampleはregeneratoRuntime.markによって包装され、レゲナートによってRuntime.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メソッドコードにより、最終的には、ローズマリーオブジェクトのmake InvokeMethod(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);
        };
      });
    }
     
    make InvokeMethodの方法の内容はわりに多くて、ここで部分を選んで分析します.まず、ジェネレータが自身の状態を「Suspended Start」に初期化することを発見しました.
     
    function makeInvokeMethod(innerFn, self, context) {
      var state = GenStateSuspendedStart;
     
      return function invoke(method, arg) {
     
    make InvokeMethodは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に入り、コード論理を実行します.得られた結果が普通のタイプの値であれば、反復可能なオブジェクト形式に包装し、ジェネレータ状態をGenStateComplettedまたはGenStation SuspendedYiedに更新します.
     
    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((zhi)の中でyieldキーワードの実現原理とほぼ一致しています.全部コンパイルして構想を変えて、状態マシンのモデルを使って、同時に関数のコンテキスト情報を保存して、最後に新しいyieldキーワードの持ってくる新しい言語の特性を実現しました.
    参考文献
    1.ES 6 Generators:Complettee Seriesシリーズの文章
    2.深入浅出ES 6 Generators
    3.「ECMAScript 6非同期プログラミングを深く把握する」シリーズの文章
    4.ES 6 Generators:How do they work?
    5.Behind the scenes of the C葃yield keyword