このページの内容
Promiseとは
Promiseは SPA(シングルページアプリケーション)を構築するうえではもはや欠かせない、非同期処理を制御するためのオブジェクトです。
- ある非同期処理が完了するのを待って、次の処理を動作させる
- 「ある非同期処理が完了するのを待って、次の処理を動作させる」を複数並行で動作させる
- 複数の非同期処理を並行で動作させ、すべて完了するのを待って、次の処理を動作させる
- 複数の非同期処理を直列で動作させる
ある非同期処理が完了するのを待って、次の処理を動作させる
サンプルの非同期処理として、下記のような関数 asyncMethod() を準備しました。
これは引数で指定されたミリ秒だけ待機したのち、文字列 id + 'finished' を解決値とするという動作をします。
この関数は、実際のシステムではサーバー上のリソースを取得するなどの処理に該当します。
function asyncMethod(id, waitTimeMillis) { return new Promise(function(resolve) { setTimeout(function(){ resolve(id + ' finished'); }, waitTimeMillis); }); }
この関数を呼び出すための関数 main1() を準備します。
function main1() { console.info('main start'); asyncMethod('A', 3000).then( function(result){ console.info(result); } ); console.info('main end'); }
main1()を呼び出してみると、開発者ツールのコンソールに下記のように表示されるはずです。
main start main end A finished
解説
main1() は、まずは console.info('main start') を処理します。
次に asyncMethod() を呼び出します。ここまでは特別なことは何もありません。
ただ、その後、その処理が完了するのを待たず、次のステップを処理します。これが非同期処理です。
ここで言う次のステップとは、then() の中ではなく、console.info('main end') です。
そして 3000ms 後にasyncMethod が解決されて、console.info(id + ' finished') が処理されます。
「ある非同期処理が完了するのを待って、次の処理を動作させる」を複数並行で動作させる
パラメータが配列に格納されているケースを考えてみます。
Array.prototype.forEach() を使って、ループ処理します。
functin main2() { console.info('main start'); var promiseArray = []; var params = [ ['A', 2000], ['B', 3000], ['C', 1000] ]; params.forEach(function(param, index){ asyncMethod(param[0], param[1]).then(function(){ console.info(result); }); }); console.info('main end'); }
main2()を呼び出してみると、下記のように表示されます。
main start main end C finished A finished B finished
よくハマる間違った実装
ここで単純に for ループのループカウンタ i を使って、下記のように実装してしまうと、意図した結果が得られないので注意が必要です。
functin main3() { console.info('main start'); var params = [ ['A', 2000], ['B', 3000], ['C', 1000] ]; for (var i = 0; i < params.length; i++) { var param = params[i]; asyncMethod(param[0], param[1]).then(function(result){ // then の外側の変数を参照すると、意図しない値が入ることがある console.info(param[0] + ' finished.'); }); }); console.info('main end'); }
main3() が回しているループカウンタと、asyncMethod()が参照しているループカウンタは、非同期処理のため一致するとは限りません。その結果、下記のように表示されてしまいます。
main start main end C finished C finished C finished
複数の非同期処理を並行で動作させ、すべて完了するのを待って、次の処理を動作させる
Promise.all を使って、下記のように実装します。
function main4() { console.info('main start'); var promiseArray = []; var params = [ ['A', 2000], ['B', 3000], ['C', 1000] ]; params.forEach(function(param, index){ promiseArray.push( asyncMethod(param[0], param[1]).then(function(result){ console.info(result); })); }); Promise.all(promiseArray).then( function(resultArray){ console.info('all done'); } ); console.info('main end'); }
main start main end C finished A finished B finished all done
もっとシンプルに
これは、Array.prototype.map() を使って、下記のように短く書くことができます。
function main5() { console.info('main start'); var params = [ ['A', 2000], ['B', 3000], ['C', 1000] ]; Promise.all( params.map(function(param){ return asyncMethod(param[0], param[1]).then(function(result){ console.info(result); }); }) ).then( function(resultArray){ console.info('all done'); } ); console.info('main end'); }
複数の非同期処理を直列で動作させる
「1つ目が終わるのを待って2つ目を起動し、2つ目が終わるのを待って3つ目を起動する」といったことを繰り返す方法です。
function main6() { console.info('main start'); var promise = Promise.resolve(); var params = [ ['A', 2000], ['B', 3000], ['C', 1000] ]; params.forEach(function(param, index){ promise = promise.then(function(){ return asyncMethod(param[0], param[1]).then(function(result){ console.info(result); }); }); }); promise.then(function(){ console.info('all done'); }); console.info('main end'); }
これを実行すると、下記のように表示されます。
main start main end A finished B finished C finished all done