JavaScriptのPromiseについて、これまでに学んだことをまとめました。
async/awaitもfetchも扱っているものはPromiseなので、JavaScriptの非同期処理に於いて、基礎のような存在と言えなくもない?
確認環境
Windows 10
Chrome DevTools 73
Sources>snippets にて確認しました。
Promiseの基本
他の方も紹介されていますが、Promiseの基礎知識についてはこちらのサイトが大変参考になります。
JavaScript Promiseの本
Promiseを使う | MDN
1箇所からデータを取得する
setTimeout
を非同期通信処理に見立てて、一つのデータを受け取り検査します。
奇数を受け取ったら通信成功、偶数もしくは数値と評価できない値の場合は通信失敗です。
// Promiseオブジェクトを作成して返す
function createPromise(param) {
// インスタンス作成時にコールバック関数を渡します。
return new Promise((resolve, reject) => {
// 非同期通信処理を定義
setTimeout((result) => {
console.log('Result:', result);
// 非同期通信で受け取った値 result を検査する
if (result % 2) {
// 成功と判断した際は1番目に受け取った関数を実行
resolve(result);
} else {
// 失敗と判断した際は2番目に受け取った関数を実行
reject(`${result} is not odd`);
}
}, 500, (param | 0));
});
}
function testPromise(param){
// 作成されたPromiseオブジェクトを使って、非同期通信後の処理を定義する
createPromise(param)
.then((result) => {
// 成功時の後続処理。resolve実行時に渡された値を受け取る
console.log('Hello Promise!:', result);
})
.catch((error) => {
// 失敗時の後続処理。reject実行時に渡された値を受け取る
console.log('Error:', error);
})
.finally(() => {
// 成功・失敗、いずれの場合も最後に実行したい処理、引数は受け取らない
console.log('Fin...');
});
}
> testPromise(5)
Result: 5
Hello Promise!: 5
Fin...
> testPromise(4)
Result: 4
Error: 4 is not odd
Fin...
Promise関数を使ってPromiseオブジェクトを作成します。
この際、結果を待ちたい非同期処理を定義したコールバック関数executor
を渡します。
このexecutor
は、引数として二つの関数を受け取ります。
一つ目の関数resolve
は、非同期処理が解決(成功)したと判断した場合に実行します。
二つ目の関数reject
は、非同期処理が拒否(失敗)したと判断した場合に実行します。
それぞれ、後続処理に渡したいデータを引数に渡します。
例えば、HTTPのレスポンスコードがHTTP 200 OK
でも、受け取った中身が不正の為に失敗と判断したい場合はreject
を実行します。
// 通信成功し、データ(responseText)を取得することは出来た。が、まだ油断は出来ない…
try {
const data = JSON.parse(responseText || null);
if (data === null) {
reject('データが無いよ');
} else {
// 受け取ったJSONデータを無事にパースできた
resolve(data);
}
} catch {
reject('JSONをパース出来なかったよ');
}
作成したPromiseオブジェクトは3つのメソッドを持ちます。それぞれに引数で解決時・拒否時・完了時に実行するコールバック関数を渡します。
各メソッドの戻り値はPromiseオブジェクトなので、メソッドチェーンで繋げることが可能です。
then
は、解決時(executor
内でresolve
実行時)に呼び出される処理onFulfilled
を一つ目の引数にとります。※
catch
は、拒否時(executor
内でreject
実行時)に呼び出される処理onRejected
を引数にとります。
また、executor
内でエラーが発生した場合もonRejected
が呼ばれます。
finally
は、解決・拒否にかかわらず、onFulfilled
またはonRejected
が実行された後に実行される処理onFinally
を引数にとります。
※ then
は、二つ目の引数にonRejected
を受け取ります。これは省略が可能です。catch
を利用せずに、then
に二つの関数を渡すことも可能です。
catch
を利用した場合との違いは、onFulfilled
内でエラーが発生した場合、then
に渡したonRejected
は実行されませんが、catch
に渡したonRejected
は実行されます。
Promise | MDN
Promise.prototype.then() | MDN
Promise.prototype.catch() | MDN
Promise.prototype.finally() | MDN
2箇所からデータを受け取る処理を直列でつなげる
ある場所から取得したデータをもとに、また別の場所へリクエストを投げたい場合は、Promiseオブジェクトを作成する処理をメソッドチェーン内で繰り返します。
一つ目のthen
内で再びcreatePromise関数を呼び出し、新たに作成したPromiseオブジェクトを戻り値に設定しています。
function createPromise(param) {
return new Promise((resolve, reject) => {
setTimeout((result) => {
console.log('Result:', result);
if (result % 2) {
resolve(result);
} else {
reject(`${result} is not odd`);
}
}, 500, (param | 0));
});
}
function testPromise(param) {
// Promiseオブジェクトを作成して最初の非同期処理Aを実行
createPromise(param)
.then((result) => {
// 処理Aが解決した場合の後続処理
// 処理Aから受け取った値を加工して次の非同期処理に利用する
const newNum = (result / 3) | 0;
console.log(`Next: ${result} to ${newNum}`);
// 新たなPromiseオブジェクトを作成して非同期処理Bを実行
return createPromise(newNum);
})
.then((result) => {
// 処理Bが解決した場合の後続処理
console.log('Hello Promise!:', result);
})
.catch((error) => {
// 処理Aまたは処理Bが拒否した場合の後続処理
console.log('Error:', error);
})
.finally(() => {
console.log('Fin...');
});
}
> testPromise(5)
Result: 5
Next: 5 to 1
Result: 1
Hello Promise!: 1
Fin...
> testPromise(7)
Result: 7
Next: 7 to 2
Result: 2
Error: 2 is not odd
Fin...
Promiseオブジェクトを作成するとresolve
,reject
の使い捨てバケツを渡され、それを使って次に荷物を受け渡す。
受け取った側は再びPromiseオブジェクトを作成してバケツを入手し、次に荷物を受け渡す。
そんなイメージでしょうか。。。
Promiseの状態
3つの状態
作成したPromiseオブジェクトは、下記三つのうちいずれかの"状態"にあります。
状態 | 条件 | 条件成立時の挙動 |
---|---|---|
pending (初期状態) | 作成時 | 待機 |
fulfilled (解決済み) | resolve()の実行 | onFulfilledを呼ぶ |
rejected (拒否済み) | reject()の実行 | onRejectedを呼ぶ |
一度状態がfulfilled
またはrejected
に変化すると、もう変わることはありません。
いずれかに変化済みの状態はsettled
と呼ばれます。
そのため、一つのオブジェクトのthen
やcatch
メソッドに登録したコールバック関数は1回しか呼ばれません。
故に使い捨てバケツであり、直列接続をするには毎回新しいPromiseを作成する必要があります。
静的メソッドのresolveとreject
静的メソッドのPromise.resolve()
, Promise.reject()
を利用すると、最初からsettled
の状態のPromiseオブジェクトを作成できます。
then()
やcatch()
内では、pending
のPromiseオブジェクトを返すとその結果を待ちますが、settled
のPromiseオブジェクトを返すとその状態に応じて後続処理が実行されます。(非同期)
Promiseオブジェクト以外のものはfulfilled
のPromiseオブジェクトにラップされ、後続のthen()
へ進みます。
後続のcatch()
へ進みたい場合はrejected
のオブジェクトを返す必要があり、Promise.reject()
を利用して作成します。
executor
内でこのメソッドを利用しても、executor
を実行したPromiseオブジェクト自体の状態は変わらないため、後続処理が実行されることはありません。
状態を変えたいオブジェクトから渡されたresolve
,rejected
を利用する必要があります。
Promise.allで複数箇所からデータを受け取る処理を並列でつなげる
取得順序は問わないが全て揃ったら次のステップに進みたい、そんなあなたのニーズにお答え出来るのが、静的メソッドのPromise.all
です。
createPromise関数の処理内容は上記と同一ですが、setTimeout関数の待機時間をランダムで1~5秒に設定されるようになっています。
function createPromise(param) {
const sec = Math.floor(Math.random() * 5 + 1) * 1000;
return new Promise((resolve, reject) => {
setTimeout((result) => {
console.log('Result:', result, `(${sec}ms)`);
if (result % 2) {
resolve(result);
} else {
reject(`${result} is not odd`);
}
}, sec, (param | 0));
});
}
function testPromise(param1, param2, param3) {
// 作成したPromiseオブジェクトの配列を渡す
Promise.all([
createPromise(param1),
createPromise(param2),
createPromise(param3),
'MoiMoi'
])
.then(([first, second, third, forth]) => {
// all()に渡した全ての処理が解決した場合、実行される
// 取得順に関係なく、all()に渡した順番の配列で結果を受け取る
console.log('Complete!:', first, second, third, forth);
})
.catch((error) => {
// いずれか一つでも拒否すると1回だけ実行される
console.log('Error:', error);
});
}
> testPromise(3,5,7)
Result: 5 (1000ms)
Result: 3 (3000ms)
Result: 7 (4000ms)
Complete!: 3 5 7 MoiMoi
> testPromise(1,2,3)
Result: 2 (1000ms)
Error: 2 is not odd
Result: 1 (2000ms)
Result: 3 (3000ms)
> testPromise(9,4,6)
Result: 4 (1000ms)
Error: 4 is not odd
Result: 9 (3000ms)
Result: 6 (5000ms)
all()
に渡す配列には、Promiseオブジェクト以外のものを要素にすることが出来ます。上記では3番目に文字列を渡しています。
この場合解決済みのPromiseとしてラップされ、後続のthen
で値を取得できます。
一度拒否が発生すると、その後に実行されたresolve()
,reject()
は無視されます。
Promise.raceで一番最初に出た結果を採用する
概要
Promise.all
に良く似た並列処理で、Promise.race
があります。
all
同様に配列で処理を渡し、並列で実行されるのを待機するのですが、いずれか一つが解決または拒否の結果を出せばそれを採用し、後続処理を実行します。
/**
* createPromise関数は上記 Promise.all のサンプルコードと同じ
*/
function testPromise(param1, param2, param3) {
// Promise.all と同様に配列を渡す
Promise.race([
createPromise(param1),
createPromise(param2),
createPromise(param3)
])
.then((result) => {
// 取得できるのは一つ
// 一番最初に帰ってきた結果を採用する
console.log('Win!:', result);
})
.catch((error) => {
console.log('Error:', error);
});
}
> testPromise(2,7,3)
Result: 7 (2000ms)
Win!: 7
Result: 2 (3000ms)
Result: 3 (3000ms)
> testPromise(2,7,3)
Result: 2 (4000ms)
Error: 2 is not odd
Result: 7 (5000ms)
Result: 3 (5000ms)
タイムアウト処理を実装する
当初、使いどころが良く分からなかったのですが、4.5. Promise.raceとdelayによるXHRのキャンセル | JavaScript Promiseの本によると、タイムアウト処理の実装に利用できることを知りました。
従来からAjax通信時に利用されてきたXMLHttpRequest(XHR)
には、標準でタイムアウトの機能があります。
XHR
の後続であるFetchAPI
には、タイムアウト機能がありません。(2019年3月現在)
もしかしたら今後は出番が増えていくのかもしれませんね。
// データ取得処理
function fetchWrp(path) {
// FetchAPIでデータを取得する
return fetch(path)
.then((response) => {
if (response.ok) {
return response.json();
} else {
return Promise.reject(response.status);
}
})
.catch((error) => {
return Promise.reject(error);
})
// DevToolでの実行用テストコード
// 1~6秒後に結果を返す
/*
const sec = Math.floor(Math.random() * 6 + 1) * 1000;
console.log(`will take ${sec}ms...`);
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`fetched (${sec}ms)`);
}, sec)
});
*/
}
// タイムアウト処理
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(`timeout (${ms}ms)`);
}, ms);
});
}
// timeoutに指定した時間内にfetchWrpの結果が出なければ、タイムアウト
Promise.race([
fetchWrp('/sample/api/data/'),
timeout(3000)
])
.then((result) => {
console.log('Result:', result);
})
.catch((error) => {
console.log('Error:', error);
});
FetchAPIでreject
fetch()
は実行するとPromiseオブジェクトを返すので、then
やcatch
を利用できます。
取得失敗時はPromise.reject
を利用することで、Promise.race
のcatch
へ処理が進むようにしています。
いろいろなメソッドチェーン
実務で利用機会があるか否かは別として、理解を深める為に書いたサンプルコードです。
単純な直列
下記の例では、executor
内で拒否されて【A】へ進んでも、その後は【B】、【C】と続き、【D】が実行されることはありません。
メソッドチェーンの途中で、Promise.reject()
の結果を返すと、後続のcatch()
が実行されます。
function testPromise(param) {
const num = param | 0;
new Promise((resolve, reject) => {
if (num % 2) {
resolve('resolve');
} else {
reject('reject');
}
})
.then((res) => {
return `${res} → hoge`;
})
.then((res) => {
return `${res} → fuga`;
})
.catch((res) => {
return `${res} → boo`; //【A】
})
.then((res) => {
return `${res} → piyo`; //【B】
// return Promise.reject(res); とした場合、【D】に進む
// resolve → hoge → fuga → rejected
})
.then((res) => {
console.log(`${res} → fulfilled`); //【C】
})
.catch((res) => {
console.log(`${res} → rejected`); //【D】
})
}
> testPromise(5)
resolve → hoge → fuga → piyo → fulfilled
> testPromise(4)
reject → boo → piyo → fulfilled
ラップされたように見えるチェーン
関数で分割されていますが、挙動としては上記のSampleAと同じです。
【B】で解決済みのPromiseオブジェクトが返されることになる為、続いて【C】が実行され、【D】は実行されません。
function createPromise(param) {
const num = param | 0;
return new Promise((resolve, reject) => {
if (num % 2) {
resolve('resolve');
} else {
reject('reject');
}
})
.then((res) => {
return `${res} → innerThen`; //【A】
})
.catch((res) => {
return `${res} → innerCatch`; //【B】
});
}
function testPromise(param) {
createPromise(param)
.then((res) => {
console.log(`${res} → outerThen`); //【C】
})
.catch((res) => {
console.log(`${res} → outerCatch`); //【D】
});
}
> testPromise(5)
resolve → innerThen → outerThen
> testPromise(4)
reject → innerCatch → outerThen
再帰関数de直列
予め用意した、取得データ検査用の関数のリストを渡して、再帰で順次実行します。
(function testPromise(fncAry, param) {
const doSomething = fncAry.shift();
if (!doSomething) {
return;
}
new Promise((resolve, reject) => {
setTimeout(doSomething, 500, param, resolve, reject);
})
.then((res) => {
console.log('Then:', res);
testPromise(fncAry, res);
})
.catch((res) => {
console.log('Catch:', res);
});
}(
[
// 1番目
(param, resolve, reject) => {
resolve('hoge');
},
// 2番目
(param, resolve, reject) => {
const rum = Math.floor(Math.random() * 10 + 1);
console.log('Number is ' + rum);
if (rum % 2) {
resolve(`${param} → fuga`);
} else {
reject(`${param} → booo`);
}
},
// 3番目
(param, resolve, reject) => {
resolve(`${param} → piyo`);
}
]
));
>
Then: hoge
Number is 9
Then: hoge → fuga
Then: hoge → fuga → piyo
>
Then: hoge
Number is 4
Catch: hoge → booo
Promiseさん家のasync・await兄弟
es2017でasync/await
が登場しました。
then()
のチェーンを使うことなく、直列処理を実現できます。
async
関数内でawait
式を使うと、そこで処理を一時停止します。
再開するには、await
式に渡されたPromiseオブジェクトが解決済みの状態になる必要があります。
再開時、await
式はPromiseにラップされていた値を返します。つまりonFulfilled
の引数で受け取る値と同じものが返されます。
Promiseオブジェクト以外のものを渡すと、その値をラップした解決済みのPromiseオブジェクトに変換される為、そのまま処理が続行します。
ただしawait
以降の実行は非同期です。
async
関数の戻り値はPromiseオブジェクトなので、then
,catch
,finally
メソッドが利用可能です。
挙動としては、上記のSampleBと似ています。
並列処理はこれまでどおりPromise.all
を利用します。
function createPromise(param) {
return new Promise((resolve, reject) => {
setTimeout((result) => {
// 値が0以下は却下
if (result > 0) {
resolve(result);
} else {
reject(`result:${result} is rejected`);
}
}, 500, (param | 0));
});
}
async function testAsync(param) {
// 1番目
const paramA = await createPromise(param);
console.log(paramA);
// 奇数は却下
if (paramA % 2) {
// Promiseの静的関数の結果を返してcatchに進む
return Promise.reject(`paramA:${paramA} is rejected`);
}
// 2番目
const paramB = await createPromise(paramA * 2);
console.log(paramB);
// 3の倍数は却下
if (!(paramB % 3)) {
// エラーを発生させて終了させる
throw new Error(`paramB:${paramB} is rejected`);
}
// 3番目
// paramBよりrumが大きいとcreatePromise内で却下される
const rum = Math.floor(Math.random() * 10 + 1);
console.log(`${paramB} minus ${rum}`);
const paramC = await createPromise(paramB - rum);
console.log(paramC);
// 4番目
// 解決済みとしてそのまま処理が続行(非同期)
const paramD = await 'wait a moment';
console.log(paramD);
// thenに進む
return 'End!';
}
function testPromise(param) {
testAsync(param)
.then((res) => {
// async関数内で戻された値を受け取る
console.log(res);
})
.catch((err) => {
// async関数内でPromiseの却下もしくはエラーの発行で実行される
console.log(err);
});
}
> testPromise(0)
result:0 is rejected
> testPromise(7)
7
paramA:7 is rejected
> testPromise(6)
6
12
Error: paramB:12 is rejected
> testPromise(2)
2
4
4 minus 5
result:-1 is rejected
> testPromise(8)
8
16
16 minus 2
14
wait a moment
End!
async function | MDN
await | MDN
Promiseさんとは仲良くなれましたでしょうか。
参考情報
【javascript】Promiseについて学ぶ①
Promiseについて0から勉強してみた
【JavaScript】ちゃんと理解しておきたいPromiseの勘所など