コールバック地獄
JavaScript プログラマにとって、コールバックを処理するのは当たり前のことですが、深すぎるコールバックを処理するのはそれほど美しくありません。次のサンプル コード スニペットでは、さらに多くの層のシナリオを考えてみましょう。伝説のコールバック地獄です。
getDirectories(function(dirs) { getFiles(dirs[0], function(files) { getContent(files[0], function(file, content) { console.log('filename:', file); console.log(content); }); }); }); function getDirectories(callback) { setTimeout(function() { callback(['/home/ben']); }, 1000); } function getFiles(dir, callback) { setTimeout(function() { callback([dir + '/test1.txt', dir + '/test2.txt']); }, 1000) } function getContent(file, callback) { setTimeout(function() { callback(file, 'content'); }, 1000) }
解決策
エコシステムには、Bluebird、Q など、コールバック地獄の問題に対処できる非同期ソリューションが多数あります。この記事では、ECMAScript 6/7 仕様での非同期プログラミングのサポートに焦点を当てます。
ES6 の約束
Promise は非同期プログラミングのソリューションであり、コールバック地獄の問題を解決する強力なツールです。
Promise は、2007 年に Dojo フレームワークに dojo.Deferred 関数が追加されたときに、JavaScript エコシステムの主流に受け入れられました。 dojo.Deferred の人気を受けて、2009 年に Kris Zyp は CommonJS Promises/A 仕様を提案しました。その後、Q.js、FuturesJS などを含む、多数の Promise 実装がエコシステムに登場しました。もちろん、Promise の人気は jQuery の存在によるところが大きいですが、jQuery は CommonJS Promises/A 仕様に完全に準拠しているわけではありません。ご覧のとおり、ES 6 仕様には Promise が含まれています。
MDN は Promise を次のように説明します:
Promise オブジェクトは、値を返すプロキシです。この戻り値は、Promise オブジェクトの作成時には不明である可能性があります。これにより、非同期操作の成功または失敗の処理方法を指定できます。 これにより、非同期メソッドは同期メソッドと同じように値を返すことができます。非同期メソッドは元の戻り値を含む
を返します。
次のコードは、Promise を通じて実装された「コールバック ヘル」セクションの例です。コードはあまり簡潔ではないように見えますが、従来の階層コールバックと比較して大幅に改善されており、コードはより保守しやすく、読みやすくなっています。
getDirectories().then(function(dirs) { return getFiles(dirs[0]); }).then(function(files) { return getContent(files[0]); }).then(function(val) { console.log('filename:', val.file); console.log(val.content); }); function getDirectories() { return new Promise(function (resolve, reject) { setTimeout(function() { resolve(['/home/ben']); }, 1000); }); } function getFiles(dir) { return new Promise(function (resolve, reject) { setTimeout(function() { resolve([dir + '/test1.txt', dir + '/test2.txt']); }, 1000); }); } function getContent(file) { return new Promise(function (resolve, reject) { setTimeout(function() { resolve({file: file, content: 'content'}); }, 1000); }); }
ES6 ジェネレーター
Promise の実装は十分に単純ではありません。より良いオプションも必要です。co はそのオプションの 1 つです。 co は、Generator に基づいた非同期フロー コントローラーです。co を理解する前に、まず Generator を理解する必要があります。 C# に精通している学生は、C# バージョン 2.0 で反復ジェネレーター用の yield キーワードが導入されたことを知っておく必要があります。 ES 6 ジェネレーターは C# に似ており、yield 構文シュガーを使用し、内部にステート マシンを実装します。具体的な使用方法については、MDN ドキュメントの関数* セクションを参照してください。原理については、Generator について詳しく理解するために AlloyTeam チームのブログを参照してください。 co を使用して ES6 Generator と ES6 Promise を巧みに組み合わせ、非同期呼び出しをより調和のとれたものにします。
co(function* (){ var dirs = yield getDirectories(); var files = yield getFiles(dirs[0]); var contentVal = yield getContent(files[0]); console.log('filename:', contentVal.file); console.log(contentVal.content); });
co は非常に賢いもので、そのコア コードは次の例のように単純化できます。もちろん、co は状態が完了するまで再帰的に実行します。
runGenerator(); function* run(){ var dirs = yield getDirectories(); var files = yield getFiles(dirs[0]); var contentVal = yield getContent(files[0]); console.log('filename:', contentVal.file); console.log(contentVal.content); } function runGenerator(){ var gen = run(); function go(result){ if(result.done) return; result.value.then(function(r){ go(gen.next(r)); }); } go(gen.next()); }
ES7 非同期/待機
ES6 ジェネレーターは確かに非常に優れていますが、残念ながらサードパーティのライブラリのサポートが必要です。良いニュースは、ES 7 では、非同期呼び出しの問題を完全に解決するために Async/Await キーワードが導入されることです。そうですね、.net は一歩先を行っており、.net Framework 4.5 が先頭に立ってサポートしています。
今後記述されるコードは次のようになります:
run(); async function run() { var dirs = await getDirectories(); var files = await getFiles(dirs[0]); var contentVal = await getContent(files[0]); console.log('filename:', contentVal.file); console.log(contentVal.content); }
結論
古典的なコールバック非同期プログラミング手法から、ES6 Promise 仕様での非同期プログラミングの改善、ES Generator と組み合わせた co のエレガントな処理、そして最後に ES7 async/await の完璧な終了まで、これによって私たちは理解することができます。なぜこれらの機能が ECMAScript に登場するのか、そしてどのような問題が解決されたのか、JavaScript 非同期プログラミングの開発傾向がより明確にわかります。