JavaScriptで非同期のメソッドをつなげる場合、コールバック関数を記述するのにネストするため、コードがだんだん右下へと流れていき、メソッドをつなげる数が多くなるほど大きなピラミッドが形成されます。(英語ではこのことをPyramid of doom(死のピラミッド)と表現しています)
1 2 3 4 5 6 7 8 9 |
step1(function (value1) { step2(value1, function(value2) { step3(value2, function(value3) { step4(value3, function(value4) { // Do something with value4 }); }); }); }); |
Promiseを使ってピラミッドを回避する
Promise
インターフェースは作成時点では分からなくてもよい値へのプロキシです。プロミスを用いることで、非同期アクションの成功や失敗に対するハンドラを関連付けることができます。これにより、非同期メソッドは、最終的な値を返すのではなく、未来のある時点で値を持つプロミスを返すことで、同期メソッドと同じように値を返すことができるようになります。https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise
特定のブラウザでは対応していない場合があるため、クライアント側で使用する場合は注意する必要があります。詳細は下記リンク参照。
http://caniuse.com/#feat=promises
こちらを確認すると、IEはEdge以外は対応していないので、クライアント側での利用はまだ控えたほうが良さそうです。
Promiseの実行サンプル
1 2 3 4 5 6 7 8 |
var promise = new Promise(function(resolve){ resolve(42); }); promise.then(function(value){ console.log(value); }).catch(function(error){ console.error(error); }); |
こちらの実行結果は以下となります。Promiseで42という値を返す処理を非同期で実行し、完了した時点でthenの処理が走ることでconsoleに出力されました。
1 |
42 |
これだけではちょっとわかりにくいので続いて以下のコードを書きました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
var promiseCount = 0; function testPromise() { var thisPromiseCount = ++promiseCount; console.log(thisPromiseCount + ') 開始 (同期処理開始)'); // 新しいプロミスを作成: 1~3秒後に結果を返すことを約束します var p1 = new Promise( // リゾルバ関数はプロミスの成功または失敗に応じて呼ばれます function(resolve, reject) { console.log(thisPromiseCount + ') プロミス開始 (非同期処理開始)'); // 非同期を作成するための一例です window.setTimeout( function() { // 約束を果たしました! resolve(thisPromiseCount); }, Math.random() * 2000 + 1000); }); // プロミスが成功した時に何をするかを定めます p1.then( // メッセージと値を記録します function(val) { console.log(val + ') プロミス成功 (非同期処理終了)'); }); console.log(thisPromiseCount + ') プロミスは作成されました (同期処理終了)'); } |
こちらのコードの出力をconsole.logにしただけですが。
testPromise()を実行した結果は以下のようになります。プロミスの成功はsetTimeoutで指定された秒数待機した後に呼ばれるので最後の行の出力だけ遅れます。
1 2 3 4 |
1) 開始 (同期処理開始) 1) プロミス開始 (非同期処理開始) 1) プロミスは作成されました (同期処理終了) 1) プロミス成功 (非同期処理終了) // setTimeoutによりこの行だけ出力が遅れる。 |
ここで気になるのはtestPromiseを連続して2回実行した場合のの結果です。
1 2 |
testPromise(); testPromise(); |
結果は以下。最後のsetTimeoutにより出力が遅れるため、先に次のtestPromiseの処理結果が出力されていることが分かりますね。
1 2 3 4 5 6 7 8 |
1) 開始 (同期処理開始) 1) プロミス開始 (非同期処理開始) 1) プロミスは作成されました (同期処理終了) 2) 開始 (同期処理開始) 2) プロミス開始 (非同期処理開始) 2) プロミスは作成されました (同期処理終了) 1) プロミス成功 (非同期処理終了) 2) プロミス成功 (非同期処理終了) |
これでピラミッドは回避できそうです。次は実践的なコードに使ってみたいところですが、IE対応できるライブラリを探してみるところらでしょうか。Angularにはそれっぽいのがあったような。
参考
JavaScript Promiseの本
http://azu.github.io/promises-book/
今更だけどPromise入門
http://qiita.com/koki_cheese/items/c559da338a3d307c9d88
MDN Promise
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise