JavaScriptのPromiseで同期処理と非同期処理のタイミングを制御する(便利)

JavaScriptでWEBプログラミングをしていると、同期処理、非同期処理が混じるのでややこしい。

例えば、同期処理が三つ続く場合だと、順番に処理が進む。

これは普通の処理だ。

一方、非同期処理が混じると、処理1から実行された非同期処理2が実行中にもかかわらず同期処理3に行ってしまう。

まあ、これで問題ない場合もあるが、同期処理3が非同期処理2の結果を利用する場合には、これでは問題が起こる。

つまり、同期処理3を開始する時点で、非同期処理2が完了している事が保証されないからだ。

例えば、以下のような例だと、

<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>

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>

ネスト(入れ子)にして、非同期処理の結果 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 (% 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 (% 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() をここに書いておけば良い。

一方、もし非同期関数 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の全体像を掴む事が出来るだとう。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を多段連結する
スポンサーリンク

シェアする

フォローする