これは何?
先達の御多聞に漏れず、JavaScriptのPromiseがなかなか理解できないので学習アウトプットを残します。
学習リソース
- メイン : javascript.infoの11章
- サブ :
- メンター : github copilot 先生 (with GPT 4o)
前提知識
- 関数基礎
- oop基礎
- 同期/非同期処理 の概要
- コールバック地獄
Promiseとは
非同期的な処理を同期的に行おうとする際、コールバック式でのコーディングではネストがどんどん深くなる。
フラットに書けるように組み込みのオブジェクトとしてPromiseが提供されている。
文字通り「約束ごとと、それが完了した結果」 を 表現したもの、と個人的には捉えている。
コンストラクタ
コンストラクタは下記の通り:
const promise = new Promise(function(resolve, reject) {
// ... 任意の実行コード
});
- コンストラクタは引数に1つのコールバック関数 : 通称executor を取る
- executor はさらにコールバック関数2つ : resolve と reject を引数に取る
- resolve と reject は JavaScriptエンジン が提供する
- コーダーはその実装を意識する必要はない
- その動作は理解する必要がある
- executor は resolve と reject のどちらかを内部でコールしないといけない
Promiseオブジェクトのプロパティ と resolve/reject の関係
Promiseは状態(state)と結果(result)を所持している。
- state は
pending / fulfilled / rejectedのいずれか。初期値はpending-
fulfilled / rejectedの両方を指してsettled(解決済み)とも呼称する
-
- result は
resolve/rejectの引数に渡した値がそのまま渡る。初期値はundefined
executor内部でresolve("hoge")とコールするとPromiseインスタンスは下記のように変わる。
- state は
fulfilled - result は
hoge
また、executor内部でreject("fuga")とコールするとPromiseインスタンスは下記のように変わる。
- state は
rejected - result は
fuga
ただし、javascript.infoでは、rejectの引数にはErrorオブジェクトを渡すことを推奨している。
技術的には、任意の型の引数で reject を呼び出すことが可能です(resolve のように)。しかし、reject (またはそれを継承したもの)では、Error オブジェクトを利用することを推奨します。
state と result は内部的な値であり、通常これらに直接アクセスすることはしない。
ただし後述の重要なthenメソッドの振る舞いに大きく関わってくる。
then関連メソッド
基本構文は下記の通り :
promise.then(
function(result) { /* 成功した結果を扱う */ },
function(error) { /* エラーを扱う */ }
);
自身のstatusがfulfilledになった場合...すなわちexecutorの内部でresolveがコールされた場合、第一引数の関数が実行される。
自身のstatusがrejectedになった場合...すなわちexecutorの内部でrejectがコールされた場合、第二引数の関数が実行される。
正常終了にのみ関心があるなら引数は一つで良い :
promise.then(
function(result) { /* 成功した結果を扱う */ }
);
異常終了にのみ関心があるなら第一引数をnullにすれば良いが、短文表現としてcatchが用意されている :
let promise = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("Whoops!")), 1000);
});
// .catch(f) は promise.then(null, f) と同じです
promise.catch(alert); // 1秒後に "Error: Whoops!" を表示
fulfilled/rejectどちらでも構わないのでとにかくsettledに解決されたとき発火したいならfinallyが使える :
new Promise((resolve, reject) => {
/* 時間のかかる処理を行い、その後 resolve/reject を呼び出す */
})
// 成功か失敗かは関係なく、promise が確定したときに実行されます
.finally(() => 読込中のインジケータを停止する )
// したがって、読み込み中のインジケータは結果/エラーを処理する前に必ず停止されます
.then(result => 結果を表示する, err => エラーを表示する)
then の戻り値
Promise.then(callback)は新たなPromiseインスタンスを返す。
ただし、new した時とは違い、ここで返ってくるPromiseインスタンスのstateはpending、resultはundefinedとは限らない。
callbackの戻り値によって、then(callback)の戻り値のPromiseインスタンスの状態は次のように決定される。
callbackの戻り値 |
thenが返すPromiseの挙動 |
|---|---|
| プリミティブ値、オブジェクトなど | その値で即resolve |
| 何も返さない | undefined で即resolve |
| Promise | 戻り値のPromiseの完了を待ち、その結果でresolve/reject |
| 例外 | rejectされる |
この性質を利用して、複数の非同期処理を手続き的に書く手法がPromiseチェーンである。
Promiseチェーン
Promiseチェーンの例 :
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000); // (*)
}).then(function(result) { // (**)
alert(result); // 1
return result * 2;
}).then(function(result) { // (***)
alert(result); // 2
return result * 2;
}).then(function(result) {
alert(result); // 4
return result * 2;
});
個人的に、この例ではthen内部のコールバックで明示のreject/resolveはいらないの?というのが躓きポイントだったが、先述の表の通り、return により暗黙でresolveされるため、次段のthenもそれによって発火する...という流れになる、と理解した。
Promiseチェーンのグッドパターン
拡張性確保のための良いプラクティスとして、thenのコールバックはPromiseを返すことが推奨されている。
チェーンの中にアロー関数を含めると大変見づらくなるので切り出す方がよい。
例 :
// 商品の注文を...
// 受注 ➡ 発送 ➡ 通知 するPromiseチェーンの例
// 各々の作業の完了の後、次の作業が始まるが、たまにミスる模様
// 英語がガバかったらご容赦...
orderRecieve("mikan")
.then(deliverly)
.then(notifySucceed)
.catch(err=>console.log(err))
function orderRecieve(itemName) {
return new Promise((resolve)=>{
setTimeout(
() => {
console.log(`order recieved : ${itemName}`)
resolve(itemName)
}
,1000
)}
)
}
function deliverly(itemName){
return new Promise((resolve,reject)=>{
// たまにミスる
if (Math.random() > 0.499) {
reject(new Error("Oops! I have eaten it!!"))
} else {
setTimeout(
() => {
console.log(`delivered : ${itemName}`)
resolve(itemName)
}
,5000
)
}
})
}
function notifySucceed(){
return new Promise((resolve)=>{
setTimeout(
() => {
console.log(`Succeed`)
resolve()
}
,5000
)
})
}
一旦ここまで
どうにかこうにか腑に落ちた...と思うのでasync/awaitのお勉強がまだですが一旦切り上げます。
苦戦したら続きとして投稿させていただきます。
誤りがあればご指摘など頂けると幸甚です。