MENU

JavaScript同期・非同期

【JavaScript】 非同期はPromise?解説が難しいので自分で理解してみた

更新日:2021/01/07

 

Promiseの概要

 

 

Promiseについて説明しようとすると難しくなるので、まずは実際の仕事におきかえてみます。

 

 

この仕事、今すぐでなくてもいいんだよな…

 

他の仕事もあるし…

 

そうだ、外部の業者に丸投げしよう!!


 

 

僕が報告を受けるのも面倒だから、受付担当を作っておこうかな…


 

resolve  reject

 

そしてお仕事を発注します

 

 

 

では、この内容でお仕事お願いしますね

 


 

 

 

受けたわまりました!

 

で、納期はいつまでに・・・?

 


 

 

 

特にないので終わったらでいいですよー

 

終わったら受付の resolveさんに報告してくださいねー

 

何か問題があったら rejectさんにお願いしますー

 


 

 

 

わかりました!

 


 

 

 

ではそのような約束(Promise)でお願いしますー

 


 

そして・・・

 

 

急ぎの仕事だけやっていればいいから、はかどるなー


 

つまりPromiseを使用すると、処理を他に丸投げして、自分の処理を続行できる機能です。

 

Promise オブジェクトとは

 

 

Promiseは、次のような二つの引数を持つ関数をメインの処理から独立して実行できるオブジェクトです。

 

function a( 引数1 , 引数2 ) {  }

 

promiseはメイン処理とは非同期で動作

 

関数が短かければ一瞬で終わります。
ですがボタン操作などの入力を待っていたりすると非同期で動いているんだと実感できます。

 

 

それはいいとして...

 

関数の実行結果をメイン処理で受け取ることはできません。

 

そのかわり二つの引数を使用して通知します。
通常Promiseで使用する関数の引数は、次のような名前で定義されます。

 

function a( resolve , reject ) {  }
    resolve : 成功時に呼び出すメソッド
    reject : 失敗時に呼び出すメソッド

 

引数は共にメソッドです。
次の例のように( )で呼び出して使用します。

 

50%の確率で成功する関数


function a( resolve , reject ){
      let per = Math.floor( Math.random() * 100 );
      (per >= 50 ) ? resolve(per) : reject(per);
}

 

resolve()reject()は、Promiseオブジェクトが用意した内部メソッドへの参照です。

 

resolve()reject()は、その内部コードで後述のPromise.prototype.thenメソッドで登録したコールバック関数を実行します。

 

つまり、『このコールバック関数内でいろいろ後処理しろ!』ということですね。

Promiseの使い方

 

Promiseを使うには、まずPromiseコンストラクターでオブジェクトを作成します。
コンストラクターって何?という人は、下の参考を見てね!
参考:【JavaScript】 コンストラクターとは?関数とは違うのか?

 

 

Promiseオブジェクトの作成

 

Promiseオブジェクトは、Promiseコンストラクターに関数を渡して作成します。

 

Promiseオブジェクト作成例


//50%の確率で成功する関数
function a( resolve , reject ){
    let per = Math.floor( Math.random() * 100 );
    (per >= 50 ) ? resolve(per) : reject(per);
}
const prm = new Promise( a );

 

 

直接関数式を渡してもOKです。

 

Promiseオブジェクト作成例2

 


const prm = new Promise(
    function ( resolve , reject ){
        let per = Math.floor( Math.random() * 100 );
        (per >= 50 ) ? resolve(per) : reject(per);
   }
);

 

 

アロー関数でもOK。

 

Promiseオブジェクト作成例3

 


const prm = new Promise(
   ( resolve , reject ) => {
        let per = Math.floor( Math.random() * 100 );
        (per >= 50 ) ? resolve(per) : reject(per);
   }
);

 

クロージャを使用して、確率を変更できるように関数化してみる。

 

Promiseオブジェクト作成例4

 


function func1( kakuritu ) {
   return new Promise( // Promiseを返す
      ( resolve , reject ) => {
           let per = Math.floor( Math.random() * 100 );
           (per >= kakuritu ) ? resolve(per) : reject(per);
      }
   );
};

const prm = func1( 30 ); // 30%でPromiseオブジェクトを作成

 

【成功・失敗コールバック関数】登録:Promise.prototype.then

 

次にresolve()reject()で呼び出される関数を登録してみます。

 

登録は、Promiseのプロトタイプオブジェクトに設定されているthen()メソッドPromise.prototype.thenを使用します。

 

構文:

Promise.prototype.then( resolve_func ,  reject_func )
    resolve_func : resolve()から呼び出される関数
    reject_func : reject()から呼び出される関数。省略可能

 

仕様上は引数名が onFulfilledonRejectedになっていますが、わかりにくいのでresolve_funcreject_funcにしてあります。

 

Promise.prototype.then使用例

 


function a( resolve , reject ){
    let per = Math.floor( Math.random() * 100 );
    (per >= 50 ) ? resolve(per) : reject(per); // (1)
}
function resolve_func( p ){ console.log( "勝ち:" + p ); } // 勝ちコールバック関数
function reject_func( p ){ console.log( "負け:" + p ); } // 負けコールバック関数

new Promise( a ).then( resolve_func , reject_func );

 

 

 

上の例のように、各コールバック関数は引数を一つ持ちます。

 

引数の値は、resolve()またはreject()に与えた値です。

 

上の例では関数a内の(1)の変数perの値が、コールバック関数に渡されます。

 

(per >= 50 ) ? resolve(per) : reject(per); // (1)

 

→resolve(per)のとき、resolve_func(per )が呼ばれる
→reject(per)のとき、reject_func(per )が呼ばれる

 

補足:

 

次のコードは、PromiseコンストラクターでPromiseインスタンスを作成し、そのインスタンスに対してthenメソッドを実行しています。

 

new Promise( a ).then( resolve_func , reject_func );

 

これは、次のように記述できます。

 

const prm = new Promise( a );
prm.then( resolve_func , reject_func );

 

 

 

例外が発生した場合

 

関数内で例外が発生した場合、reject_funcが呼び出されます。

 


function a( resolve , reject ){
    throw new Error("eror!"); // 例外をスロー
}

new Promise(a)
            .then( ()=>{} , e=>console.log("error:" , e) ); // error:Error: "eror!"!

 

 

 

 

 

【失敗関数のみ登録】:Promise.prototype.catch

 

Promise.prototype.catch()メソッドは、関数内でreject()や例外が発生したときに呼び出されるコールバック関数を登録します。

 

catch()で登録したコールバック関数は、then()で失敗時のコールバック関数が登録されていないときに呼び出されます。

 


function a( resolve , reject ){
    throw new Error("eror!"); // 強制的に例外を発生
}

new Promise(a)
            .then( ()=>{} ) // 失敗時の関数が登録されていない
            .catch(// こちらが呼び出される
                e=>console.log("catch:" , e) // catch: Error: "eror!"
            );

 

 

 

【後処理関数登録】:Promise.prototype.finally

 

Promise.prototype.finallyは、関数が成功でも失敗でも、最終的に呼び出されるコールバック関数を登録します。

 


function a( resolve , reject ){
    let per = Math.floor( Math.random() * 100 );
    return (per >= 50 ) ? resolve(per) : reject(per);
}

new Promise(a)
        .then( e => console.log("あなたの勝ち") )
        .catch( e => console.log("あなたの負け") )
        .finally( () => console.log("勝負終了") );

 

 

 

このコードを実行すると、『あなたの勝ち』または『あなたの負け』と表示した後に『勝負終了』と表示されます。

 

Promise.prototype.finallyで登録するコールバック関数には、引数を指定できません。
そのため、関数が成功したかどうかを引数から確認することができません。

 

あくまで、後処理に使用するのが目的のようです。

 

Promiseでの処理で大きなデータを使用している場合、そのまま終了するとデータが残ってしまうことがあります。
変数にnullなどを代入すると開放してくれるので、Promise.prototype.finallyを活用してください。

 

Promise.prototype.finallyは結果をスルーする

 

Promise.prototype.finallyは、thenやcatchで結果を受け取っていない場合、そのまま次に渡します。

 

 

意味が分かりません


 

説明が難しいので例を挙げます。

 


function a( resolve , reject ){
    let per = Math.floor( Math.random() * 100 );
    return (per >= 50 ) ? resolve(per) : reject(per);
}
new Promise(a)
        .finally( () => console.log("勝負終了") )
        .then( e => console.log("あなたは勝ちました(" + e + ")" ) )
        .catch( e => console.log("あなたは負けました(" + e + ")") );

 

 

 

このコードを実行すると、『勝負終了』と表示された後に『あなたの勝ち(数値)』または『あなたの負け(数値)』と表示されます。
数値は、関数aでresolve(per)またはreject(per)したときのperです。

 

これはどういうことかというと、関数aの結果に対してfinally()は何も処理をしていないということです。
そして、もしthen()catch()が後に続くなら、そちらに渡しているのです。

 

次の例は、この現象の補足です。

 


function a( resolve , reject ){
    let per = Math.floor( Math.random() * 100 );
    return (per >= 50 ) ? resolve(per) : reject(per);
}

new Promise(a)
        .then( e => console.log("あなたは勝ちました(" + e + ")" ) )
        .catch( e => console.log("あなたは負けました(" + e + ")") )
        .finally( () => console.log("勝負終了") )
        .then( e => console.log("あなたは勝ちました(" + e + ")" ) )
        .catch( e => console.log("あなたは負けました(" + e + ")") );

 

 

 

上の例はfinally()の前後にthen()catch()が記述されています。
しかし後ろのthen()catch()は、undefinedと表示されます。

 

これはfinally()の前のthen()catch()で関数aの結果を処理し、結果をundefinedに初期化しています。

 

そしてfinally()は何もせずにundefinedをスルー。
その後、then()が、undefinedを捕捉しているのです。

 

説明がよくわからないかもしれません…

 

これはPromiseチェーンという仕組みです。
Promiseチェーンはこの記事の後の方で紹介していますので、そちらを見てください。

Promiseオブジェクトのタイミング

 

Promiseオブジェクトで処理する関数は、メインの処理と独立して動作します。

 

と言われて気になるのが、いつ動作するのか。
つまりタイミングですね。

 

正確なことはJavaScriptの仕様書(ECMAScript)を解読する必要がありますが、ここでは実際の動作で確認してみます。

 

Promiseコンストラクターのタイミング

 

英語が苦手な僕がECMAScriptを解読した限りでは、Promiseコンストラクターに渡された関数はその場で実行されています。
次のコードで確認してみます。

 

Promiseで渡された関数のタイミング確認

 


console.log( "(1)start" , tim( ));

new Promise( function () {
            console.log( "(2)Promise start" , tim( ));
            setTimeout(()=>{
                console.log( "(4)Wait end" , tim( ));
            }, 3000)
        });

console.log( "(3)end" , tim( ));

 

 

 

tim()は現在の時刻を表示していて、別途次のようなコードを用意しています。

 

現在の時刻を表示


const tim = () =>{const d = new Date;return d.getHours().toString() + ':' + d.getMinutes().toString() + ':'+ d.getSeconds().toString()+ ':'+ d.getMilliseconds().toString();};

 

 

実行結果:

(1)start 16:55:24:760
(2)Promise start 16:55:24:762
(3)end 16:55:24:762
(4)Wait end 16:55:27:764

 

上の結果から、Promiseコンストラクターに渡された関数は、その場で関数が実行されているのがわかりますね。

 

つまり、"(2)Promise start"とログ表示した後、setTimeoutでタイマーを登録しています。

 

Promiseコンストラクターの処理が終わると、(3)end とログ表示してでメイン処理が終了します。

 

その後setTimeout完了時に(4)Wait endが表示されます。

 

このようにPromiseオブジェクトは、時間のかかる処理を非同期で処理してくれます。

 

 

 

Promise.prototype.thenのタイミング

 

Promise.prototype.thenは、関数の実行結果により呼び出される関数を登録するメソッドです。

 

ではthenで登録する前に、関数の処理が終わってしまったらどうなるのでしょうか?

 

次のような、resolve()が呼び出された後に、thenでコールバックが登録されるコードで確認してみます。

 


console.log( "start" , tim( ));
const a = new Promise( function ( resolve ) {
            setTimeout(()=>{ // 3秒後にresolve()を呼び出す
                console.log( "call resolve()" , tim( ));
                resolve(1);
            }, 3000);
        });

setTimeout(()=>{ // 6秒後に resolve()で呼び出すコールバックセット
            console.log( "set .then" , tim( ));
                 // thenを登録
            a.then( e => console.log("success:" + e),
                e => console.log("error:" + e));
        }, 6000);

 

 

 

実行結果:

call resolve() 18:28:22:848
set .then 18:28:25:839
success:1

 

Promise()を実行して3秒後に、resolve(1)を実行。(call resolve()
そして6秒後にthen()でコールバックを登録しています。(set .then

 

then()の実行が非常に遅いタイミングでも、問題なくコールバック関数が呼び出されました。

 

関数内でresolve()reject()が呼ばれても、thenが実行されるまで待ってくれるのです。

 

Promiseは非同期ではない

 

そもそもPromiseは非同期ではありません。
同期で実行されます。

 

 

なんだってー!!


 

ここまでの解説を否定しています。
騙された気分ですね。

 

次の例は非同期のように見えます。

 

 


console.log("Start");

new Promise(   resolve  => {
                    setTimeout( ()=> {
                       console.log("Wait End");
                       resolve(1);
                      }, 3000 );
                 })
         .then( () => console.log("Call Then"));

console.log("End");

 

 

 

結果:

Start
End
Wait End
Call Then

 

メインの処理終了後に、Promiseに渡した関数の結果が表示されているので、非同期のような気がしますね。

 

では次の例はどうなるのでしょうか。

 

 


console.log("Start");
new Promise( resolve => {

            const st = new Date().getTime();
            while( new Date().getTime() - st < 3000  ) { }

            console.log("Wait End");
            resolve(1);
        }).then( () => console.log("Call Then"));

console.log("End");

 

 

 

先ほどはsetTimeout()で3秒待ちましたが、今回は3秒経過するまでループで待っています。

 

結果:

Start
Wait End
End
Call Then

 

ループが終わるまで、メインの処理に戻っていませんね。

 

流れとして次のようになります。

 

メインの処理 → 渡した関数の処理 → メインの処理

 

しっかりと同期で処理が進んでいます。
非同期なんてどこにもありません。

 

ではなぜ最初の例が非同期に見えるのかというと、setTimeout()にresolve()reject()を処理するコールバック関数を渡して、後の処理を丸投げしているからです。
Promiseに渡した関数は、そのまま終了しているのです。

 

DOMのクリックなどのイベントや、HTTP通信などでPromiseを使用した場合も同じですね。

 

ただし、then()に登録したコールバック関数は非同期です。

 

先ほどの結果は次のようになっていました。

 

Start
Wait End
End
Call Then

 

Endが表示される時点で、resolve(1)が実行され、then()でのコールバック関数が終了しています。
この時点で、コールバック関数の呼び出しは可能となっています。

 

しかし実際にコールバック関数が呼ばれたのは、Endが表示されてメインの処理が終了した後なのです。

Promiseチェーン

 

Promiseオブジェクトには、Promiseチェーンという機能があります。

 

次のようにthen()catch()finally()を連結したものが、Promiseチェーンです。

 

new Promise(  ).then().then().then().catch().finally();

 

ウソです。

 

.then().then()はメソッドチェーン

 

上の例はPromiseチェーンを作成する一要素ではありますが、本質的には単なるメソッドチェーンです。
しかし上の例をPromiseチェーンそのものとして紹介しているケースがあって、よく理解してなかった当時の僕はいろいろ混乱しました。

 

then()メソッドは、Promiseオブジェクト内の待ち行列にコールバック関数を登録します。

 

Promiseオブジェクト then() 呼び出し

 

 

.then()の結果でthen()を処理

 

Promiseチェーンは、Promiseオブジェクトの内部的な仕組みです。

 

Promiseのインスタンスは次のような形式で作成し、そのインスタンスからthen()を呼び出して関数を待ち行列に登録します。

 

new  Promise( 関数P ).then(関数).then(関数) …

 

Promiseの引数として渡した関数P内でresolve()またはreject()が呼ばれると、待ち行列の最初のコールバック関数が実行されます。

 

Promise チェーン resolve

 

実行されたコールバック関数は、必ずPromiseオブジェクトをリターンします。
コード上でリターンしていない場合は、undefinedが適用されています。

 

リターンしたPromiseオブジェクトがDOMイベント待ちなどの場合は、その結果が出るのを待ち、次の待ち行列を処理します。

 

Promise チェーン リターン

 

このように、then()やcatch()で登録した関数がPromiseオブジェクトをリターンし、その結果によって次の関数を実行する流れがPromiseチェーンです。

 

次のコードは、Promiseチェーンを利用して「はい(OK)」「いいえ(キャンセル)」で答えられる簡単なアンケートの例です。

 

Promiseチェーンの例

 


function promiseNew( e ) {
        return new Promise( ( resolve,reject  ) => {
            confirm(e) ? resolve(1) : reject(1);
        });
    }
promiseNew("お腹がすきましたか?")
        .then( e =>  promiseNew("パスタでいいですか?"),
                e =>  promiseNew("スイーツを食べましょう!"))
        .then( e =>  alert("では行きましょう!"),
            e =>  alert("明日にしよう..."));

 

 

Promiseチェーンでプリミティブやオブジェクトをリターンする

 

次のようなコードがあるとします。

 

 


new Promise( ( resolve  ) => resolve(1) )
        .then( e => {console.log("then1:",e ); return 2; })
        .then( e => {console.log("then2:",e ); return 3; } )
        .then( e => console.log("then3:",e ) );

 

 

 

結果:

then1: 1
then2: 2
then3: 3

 

上の例では、Promiseオブジェクトではなくて数値プリミティブをリターンしています。

 

Promiseオブジェクト以外の値がリターンされると、Promise.resolve()に渡されます。
つまり、イメージとしては次のようなコードになります。

 

new Promise( ( resolve  ) => resolve(1) )
        .then( e => {console.log("then1:",e ); return Promise.resolve( 2 ); })
        .then( e => {console.log("then2:",e ); return Promise.resolve( 3 ); })
        .then( e => console.log("then3:",e ));

 

Promise.resolve()は実行結果が必ずresolveになり、次のthen()で登録した関数が実行されます。

 

Promiseチェーンで何もリターンしない

 

何もリターンしない場合、undefinedがPromise.resolve()に渡されます。

 

 


new Promise( ( resolve  ) => resolve(1) )
        .then( e => console.log( "then1:" ,e ) )
        .then( e => console.log( "then2:" ,e ) )
        .then( e => console.log( "then3:" ,e ) );

 

 

 

結果:

then1: 1
then2: undefined
then3: undefined

 

暗黙的に次のようなコードと同等です。

 

new Promise( ( resolve  ) => resolve(1) )
        .then( e => {console.log("then1:",e ); return Promise.resolve( undefined ); })
        .then( e => {console.log("then2:",e ); return Promise.resolve( undefined ); })
        .then( e => console.log("then3:",e ));

 

Promiseチェーンで例外をスローする

 

関数内で例外をスローすると、then()の2番目またはcatch()で登録した関数が実行されます。

 

 


new Promise( ( resolve  ) => resolve(1) )
        .then( e => {
            console.log("then1:",e );
            throw new Error("then1Error"); // (1)
        })
        .then( e => {console.log("then2:",e );return 3;} ,
            e => console.log("error:",e.message ) // (1)を捕捉
        )
        .then( e => console.log("then3:",e ) );

 

 

 

結果:

then1: 1
error: then1Error
then3: undefined

 

最初のthen()でErrorオブジェクトをスローしています。
その結果、2番目のthen()の二つ目の関数が呼び出されていますが、この中で何もリターンしていないため、3番目のthen()はundefinedを受け取っています。

 

catch()のあとにthen()があるとチェーンする

 

catch()は終了を意味しません。

 

catch()のあとにthen()catch()finally()があると、コールバックが実行されます。

 

 


new Promise( ( resolve  ) => resolve(1) )
        .then( e => {
            console.log("then1:",e );
            throw new Error("then1Error");
        })
        .then( e => {console.log("then2:",e );return 3;})
        .then( e => {console.log("then3:",e );return 4;} )
        .catch(e => console.log("catch:",e.message ))
        .then( e => {console.log("then4:",e );return 5;});

 

 

 

結果:

then1: 1
catch: then1Error
then4: undefined

 

どこまでも続くのね…


 

上の例ではthen4undefinedですが、catch()で登録しているコールバックで値をリターンすると、then4undefinedでなくなります。

 

例:
.catch(e => {console.log("catch:",e.message );return 1;})

 

catch()は、『Promiseチェーンの最後につけてエラーを処理するためのメソッド』のように説明されていることが多いです。
しかし本質的には、reject()に対応するコールバックのみ登録できるthen()でしかありません。

まとめ

 

今回はPromiseについて調べてみました。

 

日本語にすると約束です。
つまり約束なオブジェクトです。
ますます意味がわからなくなりました。

 

たぶんプロミスと聞くと過去を思い出して、苦手って思う日本人が多いんじゃないかと思います。

記事の内容について

 

こんにちはけーちゃんです。
説明するのって難しいですね。


「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。

裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。

そんなときは、ご意見もらえたら嬉しいです。

ご意見はこちら。
https://affi-sapo-sv.com/info.php

【お願い】

お願い

■このページのURL


■このページのタイトル


■リンクタグ