Generator関数の非同期適用

非同期プログラミングは JavaScript 言語にとって非常に重要です。 JavaScript言語の実行環境は「シングルスレッド」であり、非同期プログラミングをしないと使用できず、スタックしてしまいます。この章では主に、Generator 関数が非同期操作を完了する方法を紹介します。

従来の方法

ES6 が誕生する前には、非同期プログラミングには次の 4 つの方法がありました。

  • コールバック関数 -イベント監視
  • 公開/購読
  • プロミスオブジェクト

ジェネレーター関数は、JavaScript 非同期プログラミングをまったく新しいレベルに引き上げます。

基本概念

非同期

いわゆる「非同期」とは、タスクが連続的に完了しないことを意味し、最初の部分が最初に実行され、準備ができたら他のタスクが実行されると理解できます。 2番目の段落が再度実行されます。

たとえば、処理のためにファイルを読み取るタスクがあります。タスクの最初の部分は、ファイルの読み取りをオペレーティング システムに要求することです。次に、プログラムは他のタスクを実行し、オペレーティング システムがファイルを返すのを待ってから、タスクの 2 番目の部分 (ファイルの処理) に進みます。この不連続な実行は非同期と呼ばれます。

したがって、継続的に実行することを同期と呼びます。このプログラムは継続的に実行され、他のタスクを挿入できないため、プログラムはオペレーティング システムがハードディスクからファイルを読み取る間のみ待機できます。

コールバック関数

JavaScript 言語での非同期プログラミングの実装は、コールバック関数です。いわゆるコールバック関数は、タスクの 2 番目の段落を別の関数に記述することで、タスクが再実行されるときにこの関数が直接呼び出されます。コールバック関数の英語名「callback」は、直訳すると「呼び戻す」という意味です。

ファイルを読み込んで処理する場合はこんな感じで書きます。

fs.readFile('/etc/passwd', 'utf-8', function (err, data) {
  if (err) エラーをスローします。
  コンソール.ログ(データ);
});

上記のコードでは、readFile 関数の 3 番目のパラメーターはコールバック関数であり、タスクの 2 番目のセクションです。コールバック関数は、オペレーティング システムがファイル /etc/passwd を返すまで実行されません。

興味深い質問は、なぜ Node がコールバック関数の最初のパラメータがエラー オブジェクト err でなければならないことに同意するのかということです (エラーがない場合、このパラメータは null です)。

その理由は、実行が 2 つの段階に分かれており、最初の段階が実行された後、タスクが配置されているコンテキスト環境が終了しているためです。この後にスローされたエラーは、元のコンテキストでは捕捉できず、パラメーターとして使用して 2 番目の段落に渡すことしかできません。

約束

コールバック関数自体には問題はありません。複数のコールバック関数がネストされている場合に問題が発生します。 「A」ファイルを読み取った後、「B」ファイルを読み取ると、コードは次のようになると仮定します。

fs.readFile(fileA, 'utf-8', function (err, data) {
  fs.readFile(fileB, 'utf-8', function (err, data) {
    // ...
  });
});

3 つ以上のファイルを連続して読み込むと、多重ネストが発生することは想像に難くありません。コードは垂直方向ではなく水平方向に展開されるため、すぐに混乱して管理不能になります。複数の非同期操作は強い結合を形成するため、1 つの操作を変更する必要がある限り、その上位層のコールバック関数と下位層のコールバック関数もそれに応じて変更する必要がある場合があります。この状況を「コールバック地獄」と呼びます。

Promise オブジェクトは、この問題を解決するために提案されました。これは新しい構文機能ではなく、コールバック関数のネストをチェーン呼び出しに変更できる新しい記述方法です。 Promiseを使って複数のファイルを連続して読み込む場合の書き込み方法は以下の通りです。

var readFile = require('fs-readfile-promise');

readFile(ファイルA)
.then(関数 (データ) {
  console.log(data.toString());
})
.then(関数() {
  readFile(fileB) を返します。
})
.then(関数 (データ) {
  console.log(data.toString());
})
.catch(関数 (エラー) {
  コンソール.ログ(エラー);
});

上記のコードでは、fs-readfile-promise モジュールを使用しました。これは、readFile 関数の Promise バージョンを返します。 Promise は、コールバック関数をロードするための then メソッドと、実行中にスローされたエラーをキャッチするための catch メソッドを提供します。

Promise の記述方法は、単にコールバック関数を改良したものであることがわかります。「then」メソッドを使用すると、非同期タスクの実行の 2 つの段階がより明確にわかります。それ以外に新しい点はありません。

Promise の最大の問題は、コードの冗長性です。どのような操作が実行されても、元のタスクがパッケージ化されており、一見したところ、元のセマンティクスが非常に不明瞭になってしまいます。

それで、もっと良い書き方はないでしょうか?

ジェネレーター関数

コルーチン

従来のプログラミング言語には、長い間、非同期プログラミング ソリューション (実際にはマルチタスク ソリューション) がありました。そのうちの 1 つは「コルーチン」と呼ばれ、複数のスレッドが互いに連携して非同期タスクを完了することを意味します。

コルーチンは関数に似ており、スレッドにも似ています。その動作プロセスは大まかに以下のとおりです。

  • 最初のステップでは、コルーチン A が実行を開始します。
  • 2 番目のステップでは、コルーチン 'A' が途中で実行されて一時停止し、コルーチン 'B' に実行権が移ります。
  • 3 番目のステップ: (一定期間後) コルーチン B は実行権限を返します。
  • 4 番目のステップでは、コルーチン A が実行を再開します。

上記の処理のコルーチン「A」は 2 つ (または複数) のステージに分割して実行されるため、非同期タスクです。

たとえば、ファイルを読み取るコルーチンは次のように記述します。

関数* asyncJob() {
  // ...他のコード
  var f = yield readFile(fileA);
  // ...他のコード
}

上記のコードの関数 asyncJob はコルーチンであり、その秘密は yield コマンドにあります。ここまで実行が進むと、他のコルーチンに実行権が与えられることになります。言い換えれば、「yield」コマンドは 2 つの非同期フェーズ間の境界線です。

コルーチンは「yield」コマンドに遭遇すると一時停止し、実行権が戻るまで待機し、一時停止された場所から実行を続行します。その最大の利点は、コードが同期操作と非常によく似て記述されることです。yield コマンドを削除しても、まったく同じになります。

コルーチンのジェネレーター関数の実装

ジェネレーター関数はES6におけるコルーチンの実装であり、関数の実行権限を引き継ぐ(つまり実行を一時停止する)ことができるのが最大の特徴です。

Generator 関数全体は、カプセル化された非同期タスク、または非同期タスクのコンテナーです。非同期操作を一時停止する必要がある場合は、「yield」ステートメントで示されます。 Generator 関数は次のように実行されます。

関数* gen(x) {
  var y = 収量 x + 2;
  y を返します。
}

var g = gen(1);
g.next() // { 値: 3、完了: false }
g.next() // { 値: 未定義、完了: true }

上記のコードでは、Generator 関数を呼び出すと、内部ポインタ (つまり、トラバーサ) g が返されます。これも、Generator 関数が通常の関数と異なる点です。つまり、Generator 関数を実行すると、結果ではなくポインター オブジェクトが返されます。ポインタ gnext メソッドを呼び出すと、内部ポインタ (つまり、非同期タスク実行の最初のセクション) が最初に見つかった yield ステートメントを指すように移動します。上記の例は x + 2 まで実行されます。

つまり、「next」メソッドの機能は、「Generator」機能を段階的に実行することです。 next メソッドが呼び出されるたびに、現在のステージの情報 (value 属性と done 属性) を表すオブジェクトが返されます。 value 属性は、yield ステートメントの後の式の値で、現在のステージの値を示します。 done 属性は、ジェネレーター関数が実行されたかどうか、つまり、ジェネレーター関数が実行されたかどうかを示すブール値です。次の段階です。

ジェネレーター関数のデータ交換とエラー処理

Generator 関数は実行を一時停止および再開できます。これが、Generator 関数が非同期タスクをカプセル化できる基本的な理由です。これに加えて、非同期プログラミングの完全なソリューションとなる 2 つの機能、関数本体の内外でのデータ交換およびエラー処理メカニズムも備えています。

next の戻り値の value 属性は、Generator 関数によって出力されたデータです。また、next メソッドはパラメーターを受け取り、Generator 関数の本体にデータを入力することもできます。

関数* gen(x){
  var y = 収量 x + 2;
  y を返します。
}

var g = gen(1);
g.next() // { 値: 3、完了: false }
g.next(2) // { 値: 2、完了: true }

上記のコードでは、最初の next メソッドの value 属性は、式 x + 2 の値 3 を返します。 2 番目の next メソッドにはパラメーター 2 があり、このパラメーターは前のステージの非同期タスクの戻り結果としてジェネレーター関数に渡すことができ、関数本体の変数 y によって受け取られます。したがって、このステップの value 属性は 2 (変数 y の値) を返します。

エラー処理コードをジェネレーター関数内にデプロイして、関数の外でスローされたエラーをキャプチャすることもできます。

関数* gen(x){
  試す {
    var y = 収量 x + 2;
  } キャッチ (e){
    コンソール.ログ(e);
  }
  y を返します。
}

var g = gen(1);
g.next();
g.throw('エラーが発生しました');
// 何か問題が発生しました

上記のコードの最後の行で、ジェネレーター関数の本体の外側にあるポインター オブジェクトの throw メソッドによってスローされたエラーは、関数本体の try...catch コード ブロックによってキャッチできます。これは、エラー コードとエラー処理コードが時間と空間で分離されていることを意味します。これは間違いなく非同期プログラミングにとって非常に重要です。

非同期タスクのカプセル化

Generator 関数を使用して実際の非同期タスクを実行する方法を見てみましょう。

var fetch = require('node-fetch');

関数*gen(){
  var url = 'https://api.github.com/users/github';
  var result = yield fetch(url);
  console.log(結果.bio);
}

上記のコードでは、Generator 関数は、最初にリモート インターフェイスを読み取り、次に JSON 形式のデータから情報を解析する非同期操作をカプセル化します。前に述べたように、このコードは、「yield」コマンドが追加されている点を除けば、同期操作に非常によく似ています。

このコードを実行する方法は次のとおりです。

var g = gen();
var result = g.next();

result.value.then(関数(データ){
  data.json() を返します。
}).then(関数(データ){
  g.next(データ);
});

上記のコードでは、最初に Generator 関数を実行してトラバーサー オブジェクトを取得し、次に「next」メソッド (2 行目) を使用して非同期タスクの最初のフェーズを実行します。 Fetch モジュールは Promise オブジェクトを返すため、次の next メソッドを呼び出すために then メソッドが使用されます。

Generator 関数は非同期操作を非常に簡潔に表現しますが、プロセス管理 (つまり、いつ第 1 フェーズを実行し、いつ第 2 フェーズを実行するか) が不便であることがわかります。

サンク関数

サンク関数は、ジェネレーター関数の実行を自動化する方法です。

パラメータ評価戦略

サンク関数は 1960 年代に誕生しました。

当時、プログラミング言語は始まったばかりで、コンピュータ科学者はまだより良いコンパイラを書く方法を研究していました。議論のポイントの 1 つは「評価戦略」、つまり関数のパラメーターを正確にいつ評価する必要があるかです。

var x = 1;

関数 f(m) {
  m * 2 を返します。
}

f(x+5)

上記のコードは、最初に関数 f を定義し、次に式 x + 5 をそれに渡します。この式はいつ評価されるべきですか?

1 つの意見は、「値による呼び出し」です。つまり、関数本体に入る前に、x + 5 (6 に等しい) の値を計算し、この値を関数 f に渡します。 C 言語ではこの戦略が使用されます。

f(x+5)
// 値によって呼び出された場合は、次と同等です。
f(6)

もう 1 つの意見は、「名前による呼び出し」です。つまり、式 x + 5 を関数本体に直接渡し、それが使用されるときにのみ評価します。 Haskell 言語はこの戦略を採用しています。

f(x+5)
// 名前で呼ばれる場合は、次と同等です。
(x + 5) * 2

値による呼び出しと名前による呼び出しではどちらが優れていますか?

答えは、それぞれに長所と短所があるということです。値による呼び出しは比較的簡単ですが、パラメーターを評価する際、パラメーターは実際には使用されないため、パフォーマンスの低下が発生する可能性があります。

関数 f(a, b){
  bを返します。
}

f(3 * x * x - 2 * x - 1, x);

上記のコードでは、関数 f の最初のパラメータは複雑な式ですが、関数本体ではまったく使用されていません。このパラメータの評価は実際には不要です。したがって、コンピュータ科学者の中には、実行時のみ評価する「名前による呼び出し」を好む人もいます。

サンク関数の意味

コンパイラの「名前による呼び出し」実装では、多くの場合、パラメータを一時関数に入れてから、その一時関数を関数本体に渡します。この一時的な関数をサンク関数と呼びます。

関数 f(m) {
  m * 2 を返します。
}

f(x + 5);

// と同等

var サンク = 関数 () {
  x + 5 を返します。
};

関数 f(サンク) {
  サンク() * 2を返します。
}

上記のコードでは、関数 f のパラメータ x + 5 が関数に置き換えられます。元のパラメータが使用されるときは常に、Thunk 関数を評価するだけです。

これは、「名前による呼び出し」の実装戦略であり、式を置換するために使用されるサンク関数の定義です。

JavaScript言語のサンク関数

JavaScript 言語は値によって呼び出され、そのサンク関数には別の意味があります。 JavaScript 言語では、サンク関数は式ではなく複数パラメータ関数を置き換え、コールバック関数のみをパラメータとして受け入れる単一パラメータ関数に置き換えます。

//readFileの通常版(マルチパラメータ版)
fs.readFile(ファイル名, コールバック);

// readFile のサンク バージョン (単一パラメータ バージョン)
var サンク = 関数 (ファイル名) {
  戻り関数 (コールバック) {
    return fs.readFile(ファイル名, コールバック);
  };
};

var readFileThunk = サンク(ファイル名);
readFileThunk(コールバック);

上記のコードでは、「fs」モジュールの「readFile」メソッドはマルチパラメータ関数であり、2 つのパラメータはファイル名とコールバック関数です。コンバーターによる処理後、コールバック関数のみをパラメーターとして受け入れる単一パラメーター関数になります。この単一パラメータ バージョンはサンク関数と呼ばれます。

パラメーターにコールバック関数がある限り、どのような関数でもサンク関数の形式で記述することができます。以下は単純なサンク関数コンバーターです。

// ES5のバージョン
var サンク = 関数(fn){
  戻り関数 (){
    var args = Array.prototype.slice.call(arguments);
    戻り関数 (コールバック){
      args.push(コールバック);
      fn.apply(this, args) を返します。
    }
  };
};

// ES6のバージョン
const サンク = 関数(fn) {
  戻り関数 (...args) {
    戻り関数 (コールバック) {
      return fn.call(this, ...args, callback);
    }
  };
};

上記のコンバータを使用して、fs.readFileのサンク関数を生成します。

var readFileThunk = サンク(fs.readFile);
readFileThunk(fileA)(コールバック);

別の完全な例を次に示します。

関数 f(a, cb) {
  cb(a);
}
const ft = サンク(f);

ft(1)(console.log) // 1

サンクファイモジュール

運用環境のコンバータの場合は、Thunkify モジュールを使用することをお勧めします。

まずはインストールです。

$ npm インストール サンクファイ

使い方は以下の通りです。

var thunkify = require('thunkify');
var fs = require('fs');

var read = thunkify(fs.readFile);
read('package.json')(function(err, str){
  // ...
});

Thunkify のソース コードは、前のセクションの単純なコンバーターと非常によく似ています。

関数 thunkify(fn) {
  戻り関数() {
    var args = 新しい配列(arguments.length);
    var ctx = これ;

    for (var i = 0; i < args.length; ++i) {
      args[i] = 引数[i];
    }

    戻り関数 (完了) {
      var が呼び出されます。

      args.push(関数() {
        (呼び出された) 場合は戻ります。
        呼び出された = true;
        完了.apply(null, 引数);
      });

      試す {
        fn.apply(ctx, args);
      } キャッチ (エラー) {
        完了(エラー);
      }
    }
  }
};

そのソース コードには主に、コールバック関数が 1 回だけ実行されるようにするための追加のチェック メカニズムがあります。この設計は、以下の Generator 関数に関連しています。以下の例を参照してください。

関数 f(a, b, コールバック){
  var 合計 = a + b;
  コールバック(合計);
  コールバック(合計);
}

var ft = thunkify(f);
var print = console.log.bind(コンソール);
ft(1, 2)(印刷);
// 3

上記のコードでは、「thunkify」はコールバック関数の実行を 1 回しか許可していないため、結果は 1 行しか出力されません。

ジェネレーター機能のプロセス管理

サンク関数って何に使うの?と疑問に思うかもしれません。答えは、以前は本当に役に立たなかったのですが、ES6 の Generator 機能では、Generator 機能の自動プロセス管理に Thunk 関数が使用できるようになりました。

ジェネレーター関数は自動的に実行できます。

関数*gen() {
  // ...
}

var g = gen();
var res = g.next();

while(!res.done){
  console.log(res.value);
  res = g.next();
}

上記のコードでは、ジェネレーター関数 gen がすべてのステップを自動的に実行します。

ただし、これは非同期操作には適していません。次のステップを実行する前に前のステップを完了する必要がある場合、上記の自動実行は実現できません。そんな時に便利なのがサンク機能です。ファイルの読み取りを例に挙げます。次のジェネレーター関数は、2 つの非同期操作をカプセル化します。

var fs = require('fs');
var thunkify = require('thunkify');
var readFileThunk = thunkify(fs.readFile);

var gen = 関数* (){
  var r1 = yield readFileThunk('/etc/fstab');
  console.log(r1.toString());
  var r2 = yield readFileThunk('/etc/shells');
  console.log(r2.toString());
};

上記のコードでは、yieldコマンドを使用してプログラムの実行権をGenerator関数の外に移しているため、実行権をGenerator関数に戻すメソッドが必要です。

このメソッドは、コールバック関数内で Generator 関数に実行権限を返すことができるため、サンク関数です。理解を容易にするために、まず上記のジェネレーター関数を手動で実行する方法を見てみましょう。

var g = gen();

var r1 = g.next();
r1.value(関数 (エラー、データ) {
  if (err) エラーをスローします。
  var r2 = g.next(データ);
  r2.value(関数 (エラー、データ) {
    if (err) エラーをスローします。
    g.next(データ);
  });
});

上記のコードでは、変数 g は Generator 関数の内部ポインタであり、現在の実行ステップを示します。 next メソッドは、ポインタを次のステップに移動し、そのステップの情報 (value 属性と done 属性) を返す役割を果たします。

上記のコードを注意深く見ると、Generator 関数の実行プロセスが実際には同じコールバック関数を next メソッドの value 属性に繰り返し渡していることがわかります。これにより、再帰を使用してこのプロセスを自動化できます。

サンク機能の自動プロセス管理

サンク関数の真の能力は、ジェネレーター関数を自動的に実行する機能にあります。以下は、Thunk 関数に基づいた Generator executor です。

関数実行(fn) {
  vargen = fn();

  関数 next(err, データ) {
    var result = gen.next(data);
    if (result.done) が戻る;
    結果.値(次);
  }

  次();
}

関数* g() {
  // ...
}

実行(g);

上記のコードの「run」関数は、Generator 関数の自動実行機能です。内部の「next」関数はサンクのコールバック関数です。 next 関数は、まずジェネレーター関数の次のステップにポインターを移動し (gen.next メソッド)、次にジェネレーター関数が終了したかどうかを判断します (終了していない場合は、result.done 属性)。再度「next」関数を渡します。サンク関数 (「result.value」属性) を入力します。それ以外の場合は、直接終了します。

このエグゼキューターを使用すると、ジェネレーター関数の実行がさらに便利になります。内部に非同期操作がいくつあっても、Generator 関数を直接「run」関数に渡すだけです。もちろん、前提として、すべての非同期操作はサンク関数でなければなりません。つまり、「yield」コマンドに続くものはサンク関数でなければなりません。

var g = 関数* (){
  var f1 = yield readFileThunk('fileA');
  var f2 = yield readFileThunk('fileB');
  // ...
  var fn = yield readFileThunk('fileN');
};

実行(g);

上記のコードでは、関数「g」は「n」個の非同期ファイル読み取り操作をカプセル化します。「run」関数が実行される限り、これらの操作は自動的に完了します。このように、非同期操作は同期操作と同じように記述できるだけでなく、1 行のコードで実行することもできます。

サンク関数は、ジェネレーター関数の実行を自動化する唯一の方法ではありません。自動実行の鍵は、Generator 関数のフローを自動的に制御し、プログラムの実行権を受け取り、返すメカニズムが必要であるためです。コールバック関数はこれを行うことができ、Promise オブジェクトも同様に行うことができます。

co モジュール

基本的な使い方

co module は、2013 年 6 月に有名なプログラマー TJ Holowaychuk によってリリースされた、ジェネレーター関数の自動実行に使用される小さなツールです。

以下は、2 つのファイルを順番に読み取るジェネレーター関数です。

var gen = 関数* () {
  var f1 = yield readFile('/etc/fstab');
  var f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

co モジュールを使用すると、ジェネレーター関数のエグゼキューターを作成する手間が省けます。

var co = require('co');
コ(生成);

上記のコードでは、「co」関数が渡されている限り、Generator 関数が自動的に実行されます。

「co」関数は「Promise」オブジェクトを返すため、「then」メソッドを使用してコールバック関数を追加できます。

co(gen).then(function(){
  console.log('ジェネレーター関数の実行が完了しました');
});

上記のコードでは、Generator 関数が終了すると、プロンプトの行が出力されます。

co モジュールの原理

co が Generator 関数を自動的に実行できるのはなぜですか?

前に述べたように、Generator は非同期操作用のコンテナです。その自動実行には、非同期操作の結果が得られたときに実行権限を自動的に引き渡すことができるメカニズムが必要です。

これを行うには 2 つの方法があります。

(1) コールバック関数。非同期操作をサンク関数にラップし、コールバック関数で実行権限を返します。

(2) プロミスオブジェクト。非同期操作を Promise オブジェクトにラップし、「then」メソッドを使用して実行権限を返します。

co モジュールは、実際には 2 つの自動実行プログラム (Thunk 関数と Promise オブジェクト) を 1 つのモジュールにパッケージ化します。 co を使用するための前提条件は、Generator 関数の yield コマンドの後には Thunk 関数または Promise オブジェクトのみが続くことができるということです。配列またはオブジェクトのすべてのメンバーが Promise オブジェクトである場合は、co を使用することもできます。詳細については、以下の例を参照してください。

サンク機能に基づく自動実行機能については前節で紹介しました。 Promise オブジェクトに基づく自動実行プログラムを見てみましょう。これは co モジュールを理解するために必要です。

Promise オブジェクトに基づく自動実行

上記の例を引き続き使用します。まず、fs モジュールの readFile メソッドを Promise オブジェクトにラップします。

var fs = require('fs');

var readFile = 関数 (ファイル名){
  return new Promise(function (解決、拒否){
    fs.readFile(ファイル名, 関数(エラー, データ){
      if (エラー) が拒否 (エラー) を返します。
      解決(データ);
    });
  });
};

var gen = 関数* (){
  var f1 = yield readFile('/etc/fstab');
  var f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

次に、上記のジェネレーター関数を手動で実行します。

var g = gen();

g.next().value.then(function(data){
  g.next(data).value.then(function(data){
    g.next(データ);
  });
});

手動実行とは、実際には「then」メソッドを使用し、コールバック関数をレイヤーごとに追加することを意味します。これを理解すると、自動実行プログラムを作成できます。

関数 run(gen){
  var g = gen();

  関数 next(データ){
    var result = g.next(data);
    if (result.done) return result.value;
    result.value.then(関数(データ){
      次(データ);
    });
  }

  次();
}

実行(生成);

上記のコードでは、Generator 関数がまだ最後のステップに到達していない限り、「next」関数がそれ自体を呼び出して自動実行を実現します。

co モジュールのソースコード

co は上記の自動実行プログラムの拡張版であり、そのソース コードは数十行のみで、非常に単純です。

まず、co 関数は Generator 関数をパラメータとして受け取り、Promise オブジェクトを返します。

関数 co(gen) {
  var ctx = これ;

  return new Promise(function(resolve,拒否) {
  });
}

返された Promise オブジェクトで、co はまずパラメーター gen がジェネレーター関数であるかどうかを確認します。解決されている場合は、関数を実行して内部ポインタ オブジェクトを取得します。そうでない場合は、Promise オブジェクトの状態を返し、「解決済み」に変更します。

関数 co(gen) {
  var ctx = これ;

  return new Promise(function(resolve,拒否) {
    if (typeof gen === '関数') gen = gen.call(ctx);
    if (!gen || typeof gen.next !== 'function') returnsolve(gen);
  });
}

次に、Generator 関数の内部ポインター オブジェクトの next メソッドを onFulfilled 関数にラップします。これは主に、スローされたエラーをキャッチできるようにするためです。

関数 co(gen) {
  var ctx = これ;

  return new Promise(function(resolve,拒否) {
    if (typeof gen === '関数') gen = gen.call(ctx);
    if (!gen || typeof gen.next !== 'function') returnsolve(gen);

    onFulfilled();
    関数 onFulfilled(res) {
      var ret;
      試す {
        ret = gen.next(res);
      } キャッチ (e) {
        拒否を返します(e);
      }
      次へ(戻る);
    }
  });
}

最後に、それ自体を繰り返し呼び出す重要な next 関数があります。

関数 next(ret) {
  if (ret.done) returnsolve(ret.value);
  var value = toPromise.call(ctx, ret.value);
  if (値 && isPromise(value)) 戻り値.then(onFulfilled, onRejected);
  拒否された場合に戻ります(
    newTypeError(
      '関数、Promise、ジェネレーター、配列、またはオブジェクトのみを生成できます。'
      + 'しかし、次のオブジェクトが渡されました: "'
      + 文字列(戻り値)
      +```」
    )
  );
}

上記のコードでは、「next」関数の内部コードには合計 4 行のコマンドしかありません。

最初の行は、現在のステップが Generator 関数の最後のステップであるかどうかを確認し、そうであれば返します。

2 行目は、各ステップの戻り値が Promise オブジェクトであることを保証します。

3 行目では、then メソッドを使用して戻り値にコールバック関数を追加し、onFulfilled 関数を通じて再度 next 関数を呼び出します。

4行目では、パラメータが要件を満たさない場合(パラメータがサンク関数やPromiseオブジェクトではない場合)、Promiseオブジェクトの状態を「rejected」に変更して実行を終了する。

同時非同期操作を処理する

co は、同時非同期操作をサポートしています。これにより、特定の操作を同時に実行し、すべてが完了するまで待ってから次のステップに進むことができます。

この時点で、すべての同時操作は配列またはオブジェクトに配置され、その後に yield ステートメントが続く必要があります。

//配列の書き方
co(関数* () {
  var res = 収量 [
    Promise.resolve(1)、
    Promise.resolve(2)
  ];
  console.log(res);
}).catch(エラー時);

// オブジェクトの書き方
co(関数* () {
  var res = 収量 {
    1: Promise.resolve(1)、
    2: Promise.resolve(2)、
  };
  コンソール.ログ(解像度);
}).catch(エラー時);

別の例を示します。

co(関数* () {
  var 値 = [n1, n2, n3];
  yield value.map(somethingAsync);
});

function* somethingAsync(x) {
  // 非同期で何かを行う
  yを返す
}

上記のコードでは、3 つの somethingAsync 非同期操作を同時に実行でき、すべてが完了するまで次のステップに進みません。

例: 処理ストリーム

ノードは、データの読み取りと書き込みにストリーム モードを提供します。これは、データをチャンクに分割し、まさに「データ フロー」のように、一度にデータの一部だけを処理することを特徴とします。これは、大規模なデータを処理する場合に非常に役立ちます。ストリーム モードは EventEmitter API を使用し、3 つのイベントをリリースします。

  • data イベント: 次のデータ ブロックの準備ができています。
  • end イベント: 「データ フロー」全体が処理されました。
  • error イベント: エラーが発生しました。

「Promise.race()」関数を使用すると、これら 3 つのイベントのどれが最初に発生するかを判断できます。「data」イベントが最初に発生した場合にのみ、次のデータ ブロックが処理されます。したがって、「while」ループを通じてすべてのデータの読み取りを完了できます。

const co = require('co');
const fs = require('fs');

const stream = fs.createReadStream('./les_miserables.txt');
letvaljeanCount = 0;

co(関数*() {
  while(true) {
    const res = yield Promise.race([
      new Promise(resolve => stream.once('data',solve)),
      new Promise(resolve => stream.once('end',solve)),
      new Promise((解決、拒否) => stream.once('エラー'、拒否))
    ]);
    if (!res) {
      壊す;
    }
    stream.removeAllListeners('データ');
    stream.removeAllListeners('end');
    stream.removeAllListeners('error');
    valjeanCount += (res.toString().match(/valjean/ig) || []).length;
  }
  console.log('count:', valjeanCount); // カウント: 1120
});

上記のコードは、ストリーム モードを使用して「レ ミゼラブル」のテキスト ファイルを読み取り、データ ブロックごとに stream.once メソッドを使用し、dataend、と error 。変数 res は、data イベントが発生したときのみ値を持ち、その後、各データ ブロック内の単語 valjean の出現数が累積されます。


作者: wangdoc

アドレス: https://wangdoc.com/

ライセンス: クリエイティブ・コモンズ 3.0