【JavaScript】 非同期はPromise?解説が難しいので自分で理解してみた
更新日:2024/02/27
JavaScriptで非同期するならPromiseオブジェクトが楽です。
ですがネット上での解説ってわざと難しく書いているように気がします。
そこで今回は、Promiseオブジェクトの使い方を僕なりに解説してみます。
Promiseの概要
僕がこの記事を最初に書いたころは、正直に言うとあまりPromiseを理解していませんでした。
そのためPromiseを使用しているうちに見識の違いに気づき、その都度、記事を大幅に改稿してきました。
そして最終的に次のような結論の行き着きました。
■Promiseとは
Promiseは任意のタイミングでタスクキューにタスクを追加できます。
その結果として、プログラムコードを非同期で実行できます。
上記の内容を理解するには、JavaScriptのイベントループとタスクについて知っておく必要があります。
JavaScriptのイベントループとタスクの概要
JavaScriptにはイベントループという仕組みがあります。
イベントループはタスクキューを監視していて、キューにタスクが追加されると順番に処理していきます。
タスクにはコールバック関数の呼び出しが含まれていて、JavaScriptコード目線ではコールバック関数が非同期で処理されたことになります。
詳しくはこちらを参考にしてみてください。
■非同期とイベントループ | 【JavaScript】 並列処理と非同期/同期処理の違い
Promiseとタスク
Promiseもタイマー処理などと同じように、タスクキューにタスクを追加します。
次のコードは、もっとも単純なPromiseの初期化コードです。
new Promise( resolve => {
resolve(); // ←タスクキューにタスクが追加される
});
Promiseには、上のコードと同じ結果となる次のメソッドが用意されています。
Promise.resolve()
※newは不要
Promiseの初期化は、コンストラクターで行います。
コンストラクターは引数としてコールバック関数を二つ受け取り、コンストラクターの処理内で実行されます。
(上記のコードは単純化のため、引数を一つだけ記述しています。)
コールバック関数の引数は二つとも実行可能な関数オブジェクトです。
この関数を実行すると、タスクキューにタスクが追加されます。
処理系によってはタスクキューよりも優先度が高いマイクロタスクキューを用意して、そちらに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 );
引数のresolveとrejectは、JavaScriptの実行可能な関数オブジェクトです。
resolveは解決、rejectは拒否という意味があります。
Promiseは約束という意味なので、それに関連した言葉が使用されているようですが、Promiseがわかり難い原因でもあります。
コールバック関数を処理した結果が正常終了ならresolveを実行して、エラーならrejectを実行すると覚えておくといいです。
ここではresolveとrejectの両方を呼び出す例として、上記のようなコードを作成しています。
しかしあまり良い例とは言えません。
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オブジェクト内の待ち行列にコールバック関数を登録します。
.then()の結果でthen()を処理
Promiseチェーンは、Promiseオブジェクトの内部的な仕組みです。
Promiseのインスタンスを作成し、そのインスタンスからthen()を呼び出して関数を待ち行列に登録します。
new Promise( 関数P ).then(関数).then(関数) …
Promiseの引数として渡した関数P内でresolve()またはreject()が呼ばれると、待ち行列の最初のコールバック関数が実行されます。
実行されたコールバック関数は、必ずPromiseオブジェクトをリターンします。
コード上でリターンしていない場合は、undefinedが適用されています。
リターンしたPromiseオブジェクトがDOMイベント待ちなどの場合は、その結果が出るのを待ち、次の待ち行列を処理します。
このように、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
どこまでも続くのね…
上の例ではthen4がundefinedですが、catch()で登録しているコールバックで値をリターンすると、then4はundefinedでなくなります。
例:
.catch(e => {console.log("catch:",e.message );return 1;})
catch()は、『Promiseチェーンの最後につけてエラーを処理するためのメソッド』のように説明されていることが多いです。
しかし本質的には、reject()に対応するコールバックのみ登録できるthen()でしかありません。
まとめ
今回はPromiseについて調べてみました。
日本語にすると約束です。
つまり約束なオブジェクトです。
ますます意味がわからなくなりました。
たぶんプロミスと聞くと過去を思い出して、苦手って思う日本人が多いんじゃないかと思います。
Promiseは理解するととても単純です。
悩みすぎに注意しましょう。
更新日:2024/02/27
関連記事
スポンサーリンク
記事の内容について
こんにちはけーちゃんです。
説明するのって難しいですね。
「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。
裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。
掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。
ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php
このサイトは、リンクフリーです。大歓迎です。