1. コールバック ピラミッドと理想的なソリューション
JavaScript がシングルスレッドの非同期ノンブロッキング言語であることは誰もが知っています。非同期ノンブロッキングは確かに利点の 1 つですが、多数の非同期操作には必然的に多数のコールバック関数が含まれることになり、特に非同期操作がネストされている場合にはコールバック ピラミッドの問題が発生し、コードの可読性が非常に低くなります。たとえば、次の例:
var fs = require('fs'); fs.readFile('./file1', function(err, data) { console.log(data.toString()); fs.readFile('./file2', function(err, data) { console.log(data.toString()); }) })
この例では、2 つのファイルの内容を連続して読み取り、それらを出力します。file1 の読み取りが完了した後に file2 の読み取りを実行する必要があるため、この操作は file1 のコールバック関数で実行する必要があります。読む。 。これは典型的なコールバックのネストであり、実際のプログラミングでは、さらに多くのレベルのネストが発生する可能性がありますが、間違いなく十分にエレガントではありません。
私たちの想像では、より洗練された書き方は、次のような、同期的に見えて実際には非同期である書き方であるはずです:
var data; data = readFile('./file1'); //下面的代码是第一个readFile执行完毕之后的回调部分 console.log(data.toString()); //下面的代码是第二个readFile的回调 data = readFile('./file2'); console.log(data.toString());
この書き方は、コールバック地獄を完全に回避します。実際、koa を使用すると、次の方法で非同期コールバック関数を作成できます:
var koa = require('koa'); var app = koa(); var request=require('some module'); app.use(function*() { var data = yield request('http://www.baidu.com'); //以下是异步回调部分 this.body = data.toString(); }) app.listen(3000);
それでは、koa はなぜそれほど魅力的なのでしょうか?
2. ジェネレーターはpromiseと連携して、非同期コールバック同期書き込みを実装します
重要な点は、前回の記事でも述べたように、ジェネレーターには「ブレークポイント」と同様の効果があるということです。 yield に遭遇すると一時停止し、yield 後に制御を関数に渡し、次回戻ったときに実行を継続します。
上記の koa の例では、yield の後に任意のオブジェクトを使用できるわけではありません。特定のタイプである必要があります。 co関数では、promise、thunk関数などをサポートできます。
今日の記事では、Promise を例として、ジェネレーターと Promise を使用して非同期同期を実現する方法を分析し、確認します。
ファイルを読み取る最初の例を使用して分析してみましょう。まず、ファイル読み取り関数を変換して、Promise オブジェクトにカプセル化する必要があります:
var fs = require('fs'); var readFile = function(fileName) { return new Promise(function(resolve, reject) { fs.readFile(fileName, function(err, data) { if (err) { reject(err); } else { resolve(data); } }) }) } //下面是readFile使用的示例 var tmp = readFile('./file1'); tmp.then(function(data) { console.log(data.toString()); })
Promise の使用について詳しくない場合は、es6 の構文を参照してください。 (近い将来、es5 構文を使用して基本的な関数を備えた Promise オブジェクトを実装する方法を説明する記事も書きますので、お楽しみに^_^)
簡単に言うと、Promise は Promise を通じてコールバック関数を実現できます。 .then(callback) の形式で。しかし、私たちの目標は、ジェネレーターと連携して、本当にスムーズな同期書き込みを実現することです。どのように連携するのですか? このコードを見てください:
var fs = require('fs'); var readFile = function(fileName) { return new Promise(function(resolve, reject) { fs.readFile(fileName, function(err, data) { if (err) { reject(err); } else { resolve(data); } }) }) } //将读文件的过程放在generator中 var gen = function*() { var data = yield readFile('./file1'); console.log(data.toString()); data = yield readFile('./file2'); console.log(data.toString()); } //手动执行generator var g = gen(); var another = g.next(); //another.value就是返回的promise对象 another.value.then(function(data) { //再次调用g.next从断点处执行generator,并将data作为参数传回 var another2 = g.next(data); another2.value.then(function(data) { g.next(data); }) })
上記のコードでは、ジェネレーターで readFile を生成し、コールバック ステートメント コードを記述します。 yield 次のコードは完全に同期しており、記事の冒頭のアイデアを実現しています。
yield の後に得られるのは、promise オブジェクトである another.value です。関数の内容は、読み取ったデータをジェネレーターに返し、ジェネレーターに継続させることです。どこでも中断して実行します。
基本的に、これは非同期コールバック同期の中心的な原理です。実際、Python に精通している人なら、Python には「コルーチン」という概念があることをご存知でしょう。これは基本的にジェネレーターを使用して実装されます。 es6 のジェネレーターが疑わしいです。Python をベースにしているだけです~)
ただし、上記のコードは手動で実行します。したがって、前の記事と同様に、ジェネレーター プロセスが自動的に実行されるように、実行関数を実装してジェネレーター プロセスを管理する必要もあります。
3. 同期コールバック関数を自動的に実行する: 実行関数の作成
ジェネレーターを手動で実行する前のコードの部分を注意深く観察すると、再帰関数を直接作成できるパターンも見つかります。代わりに:
var run=function(gen){ var g; if(typeof gen.next==='function'){ g=gen; }else{ g=gen(); } function next(data){ var tmp=g.next(data); if(tmp.done){ return ; }else{ tmp.value.then(next); } } next(); }
関数はジェネレーターを受け取り、その中での非同期実行が自動的に実行されるようにします。この run 関数を使用して、前の非同期コードを自動的に実行させます。
var fs = require('fs'); var run = function(gen) { var g; if (typeof gen.next === 'function') { g = gen; } else { g = gen(); } function next(data) { var tmp = g.next(data); if (tmp.done) { return; } else { tmp.value.then(next); } } next(); } var readFile = function(fileName) { return new Promise(function(resolve, reject) { fs.readFile(fileName, function(err, data) { if (err) { reject(err); } else { resolve(data); } }) }) } //将读文件的过程放在generator中 var gen = function*() { var data = yield readFile('./file1'); console.log(data.toString()); data = yield readFile('./file2'); console.log(data.toString()); } //下面只需要将gen放入run当中即可自动执行 run(gen);
上記のコードを実行すると、端末が file1 と file2 の内容を順番に出力していることがわかります。
ここでの run 関数は、簡単にするために Promise のみをサポートしていますが、実際の co 関数はサンクなどもサポートしていることに注意してください。
このようにして、co 関数の 2 つの主要な機能、1 つはオニオン モデルのプロセス制御、もう 1 つは非同期同期コードの自動実行です。次の記事では、これら 2 つの関数を統合し、独自の co 関数を作成します。
この記事のコードは github: https://github.com/mly-zju/async-js-demo にもあります。promise_generator.js はこの記事のサンプル ソース コードです。