JavaScriptでWEBプログラミングをしていると、同期処理、非同期処理が混じるのでややこしい。
例えば、同期処理が三つ続く場合だと、順番に処理が進む。
これは普通の処理だ。
一方、非同期処理が混じると、処理1から実行された非同期処理2が実行中にもかかわらず同期処理3に行ってしまう。
まあ、これで問題ない場合もあるが、同期処理3が非同期処理2の結果を利用する場合には、これでは問題が起こる。
つまり、同期処理3を開始する時点で、非同期処理2が完了している事が保証されないからだ。
この記事ではJavaScriptのPromiseの機能を使って非同期処理の完了を待ってから次の同期処理を実行する手法を紹介したい。
同期処理が非同期処理の結果を使う典型的な例
例えば、以下のようなhtmlだと [クリック] ボタンが表示される。
<button onclick="func1()">クリック</button> <script type="text/javascript"> function func1() { var n = func2_async(); func3(n); } function func2_async() { setTimeout(function () { return parseInt(Math.random() * 10, 10); }, 2000); } function func3(n) { alert('n=' + n); } </script>
その[クリック]ボタンをクリックすると、onclickで指定しているfunc1()が実行される。
そのfunc1() から非同期関数 func2_async() を実行する。
二秒後に0~9までの整数値が n に返って来るが、その n を表示する関数 func3() が先に実行されるので、表示結果は、
n=undefined
となる。
そういう場合は、
入れ子にして解決
func2_async() の結果が必要な関数 func3() をfunc2_async() の中に入れてしまえば解決する。
<button onclick="func1()">クリック</button> <script type="text/javascript"> function func1() { func2_async(); func4(); } function func2_async() { setTimeout(function () { var n = parseInt(Math.random() * 10, 10); func3(n); }, 2000); } function func3(n) { alert('n=' + n); } </script>
つまりfunc2_async() の中にfunc3() をネスト(入れ子)にして、setTimeout()による非同期処理の結果 n が得られた時点で func3(n) を実行すれば解決する。この例では、
n=3
などの整数が表示される。
図で書くと、こんな感じかな。
注意すべき点は、処理1が非同期処理2を開始した直後に、処理の流れは処理4に進むのだ。
なので、もし処理4が処理3の結果を使いたい場合には、ネストを何重にもする必要が出て来るのでややこしい(下図)。
俗に、コールバック地獄 (Callback Hell)と呼ばれる問題だ。
この場合、処理3は同期の場合や非同期の場合も有りうる。
Promiseを使う
こういう状況で役に立つのが Promise という機能だ。
ワテも覚えたばかりの手法なので、即席で上の例をPromiseで書いてみた。
ちょっと分かりにくいかも知れないが、こんな感じになる。
<button onclick="func1()">クリック</button> <script type="text/javascript"> function func1() { var p1 = new Promise( function (resolve, reject) { func2_async(resolve, reject); }); p1.then( function (n) { func3(n); }) .catch( function (n) { func4(n); } ); } function func2_async(resolve, reject) { setTimeout(function () { var n = parseInt(Math.random() * 10, 10); if (n % 2 === 0) resolve(n); else reject(n); }, 2000); }; function func3(n) { alert('resolve n=' + n); } function func4(n) { alert('reject n=' + n); } </script>
ややこしそうに見えるが中身は簡単だ。
まず、func1() が実行される。
その中で new Promise を実行してプロミスというのを作成するのだ。
その中に、
function (resolve, reject) { func2_async(resolve, reject); });
と書いておくと、下に示す非同期関数 func2_async() が呼び出されて実行される。
function func2_async(resolve, reject) { setTimeout(function () { var n = parseInt(Math.random() * 10, 10); if (n % 2 === 0) resolve(n); else reject(n); }, 2000); };
その非同期関数の実行完了後に乱数が偶数なら
resolve(n);
を返して、奇数なら
reject(n);
を返している。
非同期処理結果が成功か失敗かを次に実行する同期処理に伝える方法
これは、非同期関数の実行が成功した場合と失敗した場合とで、その後の処理を振り分ける必要があるので、そういう状況を偶数と奇数で表してみただけだ。
なので、もし皆さんが応用する場合には、非同期処理 func2_async() の最後にその実行結果に応じて、成功した場合には resolve() を実行し、失敗した場合には reject() を実行すれば良い。
なおこの二つの関数名は、この通り書く必要は無くて、
var p1 = new Promise( function (resolve, reject) {
の二行目の部分で function(resolve, reject) として名前を付けているので、自分で好きな名前に変えてもよいみたいだ。でも普通はこう書いてある例が多いようだ。
それで、func2_async() 実行の最後にもし resolve() を実行して呼び出し元の func1() に戻った場合には、その後、then() の中の関数 func3() が実行される。成功した場合の処理だ。
p1.then( function (n) { func3(n); })
なので、非同期関数 func2_async() が成功して結果 n が得られた時点で実行したい同期関数 func3() をここに書いておけば良い。
非同期処理結果がエラーの場合はrejectをリターンすると良い
一方、もし非同期関数 func2_async() が失敗して予想外の結果が得られた場合にはそれをエラーとして処理するために reject() を実行すると良い。
その場合には、
.catch( function (n) { func4(n); }
catch() のほうに来るのだ。ここでエラー後の処理をすれば良い。
Promise() の処理の流れを図に書いてみた。
う~ん、この例だけだと、Promiseで書いた方式では、入れ子にする方式に比べてコードも長くなってややこしいが、この後に出て来るPromise.all() を使ってみるとプロミスの便利さが良く分かると思う。
Promise.all()
複数個の非同期処理を同時に実行して、それらが全部完了した時点で次の処理をする。
そういう場合に使う機能だ。
非同期処理を5つ実行して、それぞれ1秒後、2秒後、3秒後、4秒後、5秒後に結果が返って来る例を下図に示す。
<script type="text/javascript"> var p1 = new Promise(function (resolve, reject) { setTimeout(resolve, 1000, "one"); }); var p2 = new Promise(function (resolve, reject) { setTimeout(resolve, 2000, "two"); }); var p3 = new Promise(function (resolve, reject) { setTimeout(resolve, 3000, "three"); }); var p4 = new Promise(function (resolve, reject) { setTimeout(resolve, 4000, "four"); }); var p5 = new Promise(function (resolve, reject) { setTimeout(reject, 5000, "fiveで何かエラーしたようだ"); // こちらを有効化するとp5の実行が出来ずにpromise.all 成立しない //setTimeout(resolve, 5000, "five"); // こちらを有効化すると全部成功する }); Promise.all([p1, p2, p3, p4, p5]).then(function (value) { console.log(value); alert('全部成功:' + value); }, function (value) { console.log(value); alert('何か失敗:' + value); }); </script>
この p1~p5 のプロミスの実行結果が全部そろった時点で、
- 全部成功した場合(5つの resolve が実行された)と、
- どこかで失敗した場合(どこかで reject が実行された)
で処理を分ける事が出来るのだ。
これは便利。
Promise.all() の処理の流れを図にしてみた。こんな感じかな。
さっそく、自作のプログラムで Promise() や Promise.all() を使ってみた。
動作の流れを理解できるまでに時間が掛かったが、分かってみると便利な機能だ。
皆さんもどうぞ。
まとめ
JavaScriptのPromiseを使うと、非同期処理が完了するのを待ってから次の処理を実行する事が可能になる。
非同期処理の結果が成功か失敗かに応じて戻り値を resolveかrejectをリターンすると良い。
成功なら .then() がコールされる。
失敗なら .catch() がコールされる。
引数の受け渡しも可能。
と言う事で、WEBプログラミングをやる人ならPromiseの使い方を覚えておくと非同期処理の記述が簡潔に書けるのでお勧めだ。
このページと同じ事を、jQueryの$whenと$.deferredでやる記事も書いてみたので興味ある人はそちらもご覧ください。
ワテの場合には、現状では、このjQuery方式を使う事が多い。理由は、jQuery式のほうがより簡潔な感じに書けるので。
書籍を買って読む
やはり有名なオライリー社のこの本は読むと良いだろう。これを読み通せば JavaScriptの全体像を掴む事が出来るだとう。840ページの大作なので、勉強するには十分すぎる内容だ。
でも、ワテの場合、大抵途中で挫折する。アカンがな。
JavaScriptなどWEBプログラミングの世界は、新しい技術がどんどん出て来るので、こういう新しい技術に付いて書いてある本は有り難い。
挫折せずに読み通したいもんだ。
追記(2016/5/17)
Chrome, Firefoxなら何もしなくてもPromiseが動いたのだが、IE11の場合には、以下の行をhtmlの冒頭に挿入する必要があるようだ。
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/3.2.1/es6-promise.js"> </script>
cloudflareのサイトからpromiseを動かすのに必要なjsファイルを読み込む設定だ。
これでie11でもPromiseが動くようになった。
でも逆にChrome, FFの場合にはこの行は無くても良いのだが、有る場合には無視されるのかな?
良く分からん。まあ、Chrome, FFの場合にはこの行が有っても動くので良いか。
追記(2016/5/17)
上記のPromise.all()の例では、複数実行した処理はすべて独立している。
では、処理1が成功したら処理2を実行。
処理2が成功したら処理3を実行。
処理3が成功したら処理4を実行。
のような状況も、実際の開発の現場ではありうる。
それをプロミスで書いてみたのが↓の記事。
【これは便利】JavaScriptのPromiseを多段連結する
このページでやったpromiseの手法をjQueryのwhenとdeferredでやってみたのが↓の記事。
コメント