同期・非同期

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

更新日:2024/02/27

JavaScriptで非同期するならPromiseオブジェクトが楽です。

ですがネット上での解説ってわざと難しく書いているように気がします。

そこで今回は、Promiseオブジェクトの使い方を僕なりに解説してみます。

 

Promiseの概要

僕がこの記事を最初に書いたころは、正直に言うとあまりPromiseを理解していませんでした。
そのためPromiseを使用しているうちに見識の違いに気づき、その都度、記事を大幅に改稿してきました。

そして最終的に次のような結論の行き着きました。

■Promiseとは

Promiseは任意のタイミングでタスクキューにタスクを追加できます。
その結果として、プログラムコードを非同期で実行できます。

上記の内容を理解するには、JavaScriptのイベントループとタスクについて知っておく必要があります。

JavaScriptのイベントループとタスクの概要

JavaScriptにはイベントループという仕組みがあります。
イベントループはタスクキューを監視していて、キューにタスクが追加されると順番に処理していきます。
タスクにはコールバック関数の呼び出しが含まれていて、JavaScriptコード目線ではコールバック関数が非同期で処理されたことになります。

イベントループの処理イメージ

Promiseとタスク

Promiseもタイマー処理などと同じように、タスクキューにタスクを追加します。

次のコードは、もっとも単純なPromiseの初期化コードです。


new Promise( resolve => {
    resolve(); // ←タスクキューにタスクが追加される
});

Promiseには、上のコードと同じ結果となる次のメソッドが用意されています。

Promise.resolve()
※newは不要

Promiseの初期化は、コンストラクターで行います。
コンストラクターは引数としてコールバック関数を二つ受け取り、コンストラクターの処理内で実行されます。
(上記のコードは単純化のため、引数を一つだけ記述しています。)

コールバック関数の引数は二つとも実行可能な関数オブジェクトです。
この関数を実行すると、タスクキューにタスクが追加されます。

resolveを実行するとタスクキューにタスクが追加される

処理系によってはタスクキューよりも優先度が高いマイクロタスクキューを用意して、そちらにPromiseのタスクをセットするものもあります。

任意のタイミングでのタスク追加

タスクに追加する関数は、Promiseの初期化後でも実行することができます。

次のコードは、5秒後に関数を実行しています。


new Promise( resolve => {
    setTimeout( ()=>{
        resolve();
    },5000);
});

上のコードは5秒後に、タイマー経過に関するタスクがキューに追加されます。
このタスクを処理すると、setTimeoutのコールバック関数( ()=>{ resolve();} )が実行され、Promiseのタスクがキューにセットされます。

この他にもマウスでボタンがクリックされたときや、Ajaxの通信が終了したときなど任意のタイミングでタスク追加できます。

コールバック関数の指定方法

Promiseのタスクで呼び出されるコールバック関数は、Promiseの初期化で返されたインスタンスが所持しているthenやcatchなどのメソッドでおこないます。


new Promise( resolve => {
    resolve(); // ←タスクキューにタスクが追加される
}).then( ()=>{
    // なんらかの処理
});

タスクキューに追加したタスクは、一連のプログラムコードが終了した後に順番に実行されます。
そのため上記のthenメソッドは、タスクキューからタスクが取り出される前に実行されるので、確実にコールバック関数を追加できます。

 

Promiseの使い方

Promiseの使い方について、具体的に解説してみます。

Promiseを使うには、まずPromiseコンストラクターでオブジェクトを作成します。

コンストラクターって何?という人は、下の参考を見てね!
参考:【JavaScript】 コンストラクターとは?関数とは違うのか?

Promiseオブジェクトの作成

Promiseの初期化は、Promiseコンストラクターにコールバック関数を渡して作成して、Promiseオブジェクトのインスタンスを作成します。

Promiseオブジェクト作成例


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

  // 関数aを処理するPromiseオブジェクトを作成
const prm = new Promise( a );

引数のresolverejectは、JavaScriptの実行可能な関数オブジェクトです。
resolveは解決、rejectは拒否という意味があります。
Promiseは約束という意味なので、それに関連した言葉が使用されているようですが、Promiseがわかり難い原因でもあります。

コールバック関数を処理した結果が正常終了ならresolveを実行して、エラーならrejectを実行すると覚えておくといいです。

ここではresolverejectの両方を呼び出す例として、上記のようなコードを作成しています。
しかしあまり良い例とは言えません。

rejectは、通信エラーなど、正常な処理をおこえなかったときに呼び出します。

Promiseを返す関数・メソッド

関数やメソッドを実行すると、結果としてPromiseが返ってくるものをPromiseを返す関数やメソッドと呼びます。

クロージャを使用して、前項のコードを確率を変更できるように関数化してみます。

Promiseを返す関数


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オブジェクトを作成

関数にすることで汎用的な処理ができるようになりました。
使いこなすと便利なテクニックです。

Node.jsの標準モジュールにも数多くのPromiseを返す関数・メソッドが用意されています。
基本的には同じような仕組みです。

後処理関数登録:Promise.prototype.then

初期化後のPromiseオブジェクトから呼び出すことができるthenメソッドは、resolve()reject()で呼び出されるコールバック関数を登録します。

構文:

    初期化後のPromiseオブジェクト.then( resolve_func , reject_func )
        resolve_func : resolve()から呼び出される関数
        reject_func : reject()から呼び出される関数。省略可能

戻り値:

    初期化後のPromiseオブジェクト

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_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 )が呼ばれる

例外が発生した場合

初期化のために渡したコールバック関数内で例外が発生した場合、reject_funcが呼び出されます。


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

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

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

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

構文:

    初期化後のPromiseオブジェクト.catch(  reject_func )
        reject_func : reject()から呼び出される関数。

戻り値:

    初期化後のPromiseオブジェクト

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

new Promise(a)
            .catch(
                e=>console.log("catch:" , e) // catch: Error: "****error!****"
            );

catch()の前に2番目の引数を指定たthen()が実行されている場合は、then()が優先されます。


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

new Promise(a)
            .then( ()=>{} , e=>console.log("error:" , e)  )
            .catch(
                e=>console.log("catch:" , e)
            );

2番目の引数が無ければ、catch()が有効になります。


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

new Promise(a)
            .then( ()=>{} )
            .catch(// こちらが呼び出される
                e=>console.log("catch:" , e)
            );

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

finallyは、resolve()とreject()に関係なく、呼び出されるコールバック関数を登録します。

構文:

    初期化後のPromiseオブジェクト.finally(  func )
        func : resolve()とreject()に関係なく呼び出される関数。

戻り値:

    初期化後のPromiseオブジェクト

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("勝負終了") );

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

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

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

Promiseでの処理で大きなデータを使用している場合、そのまま終了するとデータが残ってしまうことがあります。
finallyを活用してください。

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

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の結果を処理した後、結果にconsole.log()の戻り値undefinedをセットしています。

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

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

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

 

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となります。


function a(){}
console.log( a() ); // undefined

thenも同様で、戻り値を指定しないと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 ); return Promise.resolve( undefined ); })

実際には最後のthenもundefinedを返しています。
そのため、延々とPromiseチェーンを続けることができます。

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について調べてみました。

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

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

Promiseは理解するととても単純です。
悩みすぎに注意しましょう。

更新日:2024/02/27

書いた人(管理人):けーちゃん

スポンサーリンク

記事の内容について

null

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

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

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

掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。

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

 

このサイトは、リンクフリーです。大歓迎です。