node.jsで書き始めたとき、2つのことを嫌っていました。すべての一般的なテンプレートエンジンと膨大な数のコールバックです。 イベント指向サーバーの全機能を理解しているため、コールバックを自発的に使用しましたが、それ以来ジェネレーターがJavaScriptに登場し、実装される日を楽しみにしています。
そして今、この日が来ています。 現在、ジェネレーターはV8およびSpiderMonkeyで利用でき 、実装は仕様の更新に従っています-これは新しい時代の幕開けです!
V8は、ジェネレーターなどの新しいHarmony機能をコマンドラインフラグの後ろに隠しますが、これはしばらくの間です。 すべてのブラウザーで使用可能になる前に、ジェネレーターを使用して非同期コードを作成する方法を学習できます。 これらのアプローチを早めに試してみましょう。
次の安定バージョンとなるノード0.11の不安定バージョンをダウンロードすることで、今日使用できます。 ノードを開始するときに、
--harmony
または
--harmony-generators
フラグを
--harmony-generators
ます。
では、ジェネレーターを使用してコールバック地獄からどのように救うのでしょうか? ジェネレーター関数は、
yield
を使用して実行を一時停止し、再開または一時停止したときに結果を入出力します。 このようにして、関数がコールバックを渡さずに別の関数の結果を待つときに一時停止できます。
私たちの言語で言語構成を説明しようとすると、面白くないですか? コードに飛び込むのはどうですか?
ジェネレーターの基本
非同期の世界に飛び込む前に、プリミティブジェネレーターを見てみましょう。 ジェネレーターは
function*
宣言され
function*
:
function* foo(x) { yield x + 1; var y = yield null; return x + y; }
次に呼び出しの例を示します。
var gen = foo(5); gen.next(); // { value: 6, done: false } gen.next(); // { value: null, done: false } gen.send(8); // { value: 13, done: true }
クラスでメモを取った場合、次のように書きます。
-
yield
すべての式で許可されます。 - ジェネレーター呼び出しは通常の関数と同じですが、ジェネレーターオブジェクトを作成します。 ジェネレータを再開するには、
next
を呼び出すか、send
する必要があります。send
、値を送り返すときに使用されます。gen.next()
gen.send(null)
gen.next()
同等です。 ジェネレーター内で例外をスローするgen.throw
もあります。 - ジェネレーターメソッドは、純粋な元の値ではなく、
value
とdone
2つのパラメーターを持つオブジェクトを返します。done
おかげで、古いAPIにあった不便なStopIteration
例外の代わりに、return
または関数の単純な終了のいずれかでジェネレーターが終了したことが明らかになります。
非同期ソリューション#1:一時停止
どのコールバック地獄のコードをどうするか? さて、関数を任意に一時停止できる場合。 非同期コールバックコードをシュガーパン粉で同期的に見えるコードに戻すことができます。
質問:砂糖とは何ですか?
最初の解決策は、 サスペンドライブラリで提案されています。 とても簡単です。 深刻なのは、わずか16行のコードです。
これは、このライブラリでのコードの外観です。
var suspend = require('suspend'), fs = require('fs'); suspend(function*(resume) { var data = yield fs.readFile(__filename, 'utf8', resume); if(data[0]) { throw data[0]; } console.log(data[1]); })();
suspend
関数は、ジェネレーターを起動する通常の関数にジェネレーターを渡します。
resume
関数をジェネレータに渡します。
resume
関数はすべての非同期呼び出しのコールバックとして使用する必要があり、エラーと値のフラグを含む引数でジェネレータを再開します。
resume
とジェネレーターのダンスは興味深いですが、いくつかの欠点があります。 第一に、返される2要素配列は、構造化(
var [err, res] = yield foo(resume)
)しても不便です。 値のみを返し、エラーがある場合は例外としてスローします。 実際、ライブラリはこれをサポートしていますが、オプションとして、それがデフォルトであるべきだと思います。
第二に、常に明示的に履歴書を渡すのは不便であり、さらに、上記の機能が完了するまで待つと不適切です。 そして、通常はノードで行われるように、
callback
を追加して、関数の最後に呼び出す必要があります。
最後に、たとえば複数の同時呼び出しで、より複雑な実行スレッドを使用することはできません。 READMEは 、他のフロー制御ライブラリがすでにこの問題を解決していると主張しています。
suspend
とそれらのいずれかを使用
suspend
必要がありますが、ジェネレーターサポートを含むフロー制御ライブラリが必要です。
著者からの追加: kriskowalはcreationixによって書かれたこの要点を提案しました。コールバックベースのコード用に改善されたスタンドアロンジェネレーターハンドラーがあります。 デフォルトでエラーをスローするのはとてもクールです。
非同期ソリューション#2:約束
非同期の実行スレッドを制御するより興味深い方法は、promiseを使用することです。 Promiseは、将来の価値を表すオブジェクトであり、非同期動作を表すプログラムによる実行の呼び出しスレッドにPromiseを提供できます。
ここで約束を説明しません。時間がかかりすぎます。また、すでに良い説明があります。 最近、ライブラリ間相互作用の振る舞いとAPI約束の定義に重点が置かれていますが、その考え方は非常に単純です。
事前ジェネレーターのサポートが既にあり、また非常に成熟しているため、約束のためにQライブラリーを使用します。 task.jsはこのアイデアの初期の実装でしたが、 promiseの非標準の実装がありました。
一歩後退して、実際の例を見てみましょう。 多くの場合、単純な例を使用します。 このコードは、メッセージを作成してから受信し、同じタグを使用してメッセージを受信します(
client
はredisのインスタンスです):
client.hmset('blog::post', { date: '20130605', title: 'g3n3rat0rs r0ck', tags: 'js,node' }, function(err, res) { if(err) throw err; client.hgetall('blog::post', function(err, post) { if(err) throw err; var tags = post.tags.split(','); var posts = []; tags.forEach(function(tag) { client.hgetall('post::tag::' + tag, function(err, taggedPost) { if(err) throw err; posts.push(taggedPost); if(posts.length == tags.length) { // - post taggedPosts client.quit(); } }); }); }); });
この例の見苦さをご覧ください! コールバックは、画面の右側にあるコードをすばやく押します。 さらに、すべてのタグをリクエストするには、各リクエストを手動で管理し、すべてのタグが準備できたことを確認する必要があります。
このコードをQ promiseに持ち込もう。
var db = { get: Q.nbind(client.get, client), set: Q.nbind(client.set, client), hmset: Q.nbind(client.hmset, client), hgetall: Q.nbind(client.hgetall, client) }; db.hmset('blog::post', { date: '20130605', title: 'g3n3rat0rs r0ck', tags: 'js,node' }).then(function() { return db.hgetall('blog::post'); }).then(function(post) { var tags = post.tags.split(','); return Q.all(tags.map(function(tag) { return db.hgetall('blog::tag::' + tag); })).then(function(taggedPosts) { // - post taggedPosts client.quit(); }); }).done();
redis関数をラップする必要があったため、コールバックベースをプロミスベースに変えました。これは簡単です。 promiseを取得したらすぐに
then
を呼び出して、非同期操作の結果を待ちます。 詳細については、promises / A +仕様で説明されています。
Q
は、
all
などのいくつかの追加メソッドを実装します。プロミスの配列を受け取り、それぞれが完了するまで待機します。 さらに、
done
があります。これは、非同期プロセスが完了し、未処理のエラーがスローされることを示しています。 promises / A +仕様によると、すべての例外はエラーに変換され、エラーハンドラーに渡される必要があります。 したがって、エラーがない場合、すべてのエラーが確実にスローされます。 (不明な点がある場合は、ドミニクのこの記事をお読みください。)
最終的な約束の深さに注目してください。 これは、最初に
post
にアクセスし、次に
taggedPosts
アクセスする必要があるため
taggedPosts
。 コールバックスタイルのコードがここで感じられ、迷惑です。
今こそ、発電機の能力を評価するときです。
Q.async(function*() { yield db.hmset('blog::post', { date: '20130605', title: 'g3n3rat0rs r0ck', tags: 'js,node' }); var post = yield db.hgetall('blog::post'); var tags = post.tags.split(','); var taggedPosts = yield Q.all(tags.map(function(tag) { return db.hgetall('blog::tag::' + tag); })); // - post taggedPosts client.quit(); })().done();
それは素晴らしいことではありませんか? これは実際にどのように起こりますか?
Q.async
は、ジェネレーターを
Q.async
、サスペンドライブラリと同様に、ジェネレーターを制御する関数を返します。 ただし、ここでの重要な違いは、ジェネレーターがプロミスを与える(譲る)ことです。 Qはすべての約束を受け入れ、ジェネレーターをそれに関連付け、約束が満たされると再開し、結果を送り返します。
厄介な
resume
機能を管理する必要はありません。Promisesは完全にそれを処理し、 Promises動作の利点を活用します。
利点の1つは、必要に応じて異なるQプロミスを使用できることです。たとえば、いくつかの非同期操作を並行して実行する
Q.all
です。 この方法により、ジェネレーターで同様のQ Promiseと暗黙的なPromiseを簡単に組み合わせて、非常にきれいに見える複雑な実行スレッドを作成できます。
また、ネストの問題はまったくありません。
post
と
taggedPosts
は同じスコープに留まるため、
taggedPosts
でスコープのチェーンが壊れる心配はもうありません。
エラー処理は非常に難しいため、ジェネレーターでプロミスを使用する前にプロミスがどのように機能するかを本当に理解する必要があります。 promiseのエラーと例外は常にエラー処理関数に渡され、例外をスローすることはありません。
async
ジェネレーターはすべて例外ではありません。 エラーコールバック:
someGenerator().then(null, function(err) { ... })
エラーを管理できます。
ただし、ジェネレーターpromisesには特別な動作があります。つまり、ジェネレーターが中断されたポイントを除き、特別な
gen.throw
メソッドを使用してジェネレーターにスローされた
gen.throw
からのエラーはスローされます。 つまり、
try/catch
を使用してジェネレーターのエラーを処理できます。
Q.async(function*() { try { var post = yield db.hgetall('blog::post'); var tags = post.tags.split(','); var taggedPosts = yield Q.all(tags.map(function(tag) { return db.hgetall('blog::tag::' + tag); })); // - post taggedPosts } catch(e) { console.log(e); } client.quit(); })();
これは期待どおりに機能します
db.hgetall
呼び出しからのエラーは、
Q.all
内の深い約束のエラーであっても、
catch
ハンドラで処理されます。
try/catch
なければ
try/catch
例外はプロミスの呼び出し元のエラーハンドラーに渡されます(呼び出し元がいない場合、エラーは抑制されます)。
考えてみてください- 非同期コードのtry / catchで例外ハンドラーをインストールできます。 エラーハンドラの動的スコープは正しいものになります。
try
ブロックの実行中に発生する未処理のエラーは
catch
に渡されます。
finally
的に、エラーハンドラーがなくても、エラーが発生した場合でも、起動時に自信を持って「クリーンアップ」コードを作成できます。
さらに、promiseを使用するときはいつでも
done
を使用します。これにより、非同期コードで頻繁に発生するエラーを静かに無視する代わりに、デフォルトでエラーをスローできます。
Q.async
使用
Q.async
は通常、次のとおりです。
var getTaggedPosts = Q.async(function*() { var post = yield db.hgetall('blog::post'); var tags = post.tags.split(','); return Q.all(tags.map(function(tag) { return db.hget('blog::tag::' + tag); })); });
上記は、単にプロミスを作成し、エラー処理を処理しないライブラリコードです。 次のように呼び出します:
Q.async(function*() { var tagged = yield getTaggedPosts(); // - tagged })().done();
これはトップレベルのコードです。 前述したように、
done
メソッドは、未処理のエラーに対して例外としてエラーをスローすることが保証されています。 このアプローチは一般的だと思いますが、追加のメソッドを呼び出す必要があります。
getTaggedPosts
は、promise生成関数によって使用されます。 上記のコードは、約束で満たされた単純なトップレベルのコードです。
プルリクエストでQ.spawnを提案しましたが、これらの変更はすでにQにヒットしています! これにより、promiseを使用するコードを簡単に実行できます。
Q.spawn(function*() { var tagged = yield getTaggedPosts(); // - tagged });
spawn
はジェネレータを受け取り、すぐに起動し、未処理のエラーをすべて自動的にスローします。 これは
Q.done(Q.async(function*() { ... })())
とまったく同じです。
その他のアプローチ
約束されたベースのジェネレーターコードが形になり始めます。 砂糖の粒と一緒に、非同期ワークフローに関連する余分な荷物をたくさん取り除くことができます。
ジェネレーターを使用してしばらくしてから、いくつかのアプローチを強調しました。
価値がない
1つのプロミスだけを待つ必要がある短い関数がある場合、ジェネレーターを作成する価値はありません。
var getKey = Q.async(function*(key) { var x = yield r.get(dbkey(key)); return x && parseInt(x, 10); });
このコードを使用してください:
function getKey(key) { return r.get(dbkey(key)).then(function(x) { return x && parseInt(x, 10); }); }
私は最新バージョンがきれいに見えると思います。
spawnMap
これは私がよくやったことです:
yield Q.all(keys.map(Q.async(function*(dateKey) { var date = yield lookupDate(dateKey); obj[date] = yield getPosts(date); })));
Q.all(arr.map(Q.async(...)))
を行う
spawnMap
を
spawnMap
して
Q.all(arr.map(Q.async(...)))
と便利な場合があります。
yield spawnMap(keys, function*(dateKey) { var date = yield lookupDate(dateKey); obj[date] = yield getPosts(date); })));
これは、 非同期ライブラリの
map
メソッドに似てい
map
。
asyncCallback
最後に気づいたのは、
Q.async
関数を作成して、すべてのエラーをスローさせたい場合があります。 これは、express:
app.get('/url', function() { ... })
などの異なるライブラリからの通常のコールバックで発生します。
上記のコールバックを
Q.async
関数に変換することはできません。すべてのエラーが静かに抑制されるため、すぐに実行されないため
Q.spawn
も使用できません。 たぶん、
asyncCallback
ようなものがいいでしょう:
function asyncCallback(gen) { return function() { return Q.async(gen).apply(null, arguments).done(); }; } app.get('/project/:name', asyncCallback(function*(req, res) { var counts = yield db.getCounts(req.params.name); var post = yield db.recentPost(); res.render('project.html', { counts: counts, post: post }); }));
まとめとして
ジェネレーターを調べたとき、ジェネレーターが非同期コードに役立つことを本当に期待していました。 そして、判明したように、それらは実際に機能しますが、それらをジェネレーターと効果的に組み合わせるには、Promisesの仕組みを理解する必要があります。 promiseを作成すると、暗黙的がさらに暗黙的になるため、promise全体を理解するまでasyncまたはspawnを使用することはお勧めしません。
これで、非同期動作をコーディングするための簡潔で信じられないほど強力な方法が得られ、FSを操作するための操作をより美しくするだけでなく、それを使用することができます。 実際、同期を維持しながら、異なるプロセッサまたはマシンで実行できる簡潔な分散コードを作成する優れた方法があります。
著者からの補遺:次の記事、 「約束のないジェネレーターを見る」を読んでください 。