【JavaScript】非同期処理について(Promise)
はじめに
こんにちは。SHIFTのDevOps推進部に所属しているozakiです。
前のブログでコールバック関数について整理しました。
今回はその学びの続きとして、Promiseについてまとめていこうと思います。
Promiseとは
Promiseは、非同期処理の状態や結果を表現するビルドインオブジェクトです。言い換えると、非同期処理を簡単に記述するために、コア言語(ECMASscript)に導入された機能と言えます。
非同期処理はPromiseオブジェクトを返し、そのPromiseオブジェクトには状態変化をした際に呼び出されるコールバック関数を登録できます。
Promiseの利点は、
コールバック地獄をpromiseチェーンとして読みやすい形で表現できる
値を返すことができる
エラーを処理する方法が標準化されている
ことです。
Promiseの注意点は、
単一の非同期の処理結果を表す
つまり、繰り返し行われる非同期処理は表せない
ことです。
例えば、setTimeout()は表現できても、setInterval()を表現することは想定されていません。
Promiseの使い方
それでは、具体的なPromiseの使い方についてみていきます。
// 例1
function examplePromise() {
return new Promise((resolve, reject) => {
// 非同期処理A
});
}
examplePromise().then(()=> {
console.log(resolve);
}).catch(() => {
console.log(reject);
});
examplePromiseを呼び出すと、中で定義している処理が実行されます。その処理が行われている間に、Promiseオブジェクトが返されます。Promiseオブジェクトには、then()やcatch()というメソッドが定義されており、このメソッドにコールバック関数を渡します。
この例では、非同期処理Aが成功した場合はresolveが、失敗した場合rejectが呼ばれます。そして、非同期処理Aが成功していればthen()、失敗していればcatch()の処理が行われます。
このthen()やcatch()がPromiseの特徴的な部分です。
Promiseの状態
Promiseの基本的な形・使い方を知りました。ここからもう少し詳しく見ていこうと思います。がその前に、Promiseのややこしい部分について、整理していこうと思います。
先ほど「非同期処理Aが成功していればthen()、失敗していればcatch()の処理が行われます。」と書きました。これは、成功・失敗によってPromiseオブジェクトの状態が変化することで判断されています。このPromiseオブジェクトの状態は定義されており、3つの状態があります。
fulfilled
例1で処理が成功(resolve)した状態
(then()が呼び出されたときの状態)
rejected
処理が失敗(reject)した状態
(catch()が呼び出されたときの状態)
pending
fulfilledでもrejectedでもない状態
そして、fulfilledかrejectedの状態であることをsettled(完了している)と言います。なお、Promiseはfulfilledかrejectedのどちらかにしかなりませんし、1度決まれば変わることはありません。
最初に注意点としてPromiseは非同期で実行される処理の結果を表すと述べました。Promiseオブジェクトがfulfilledの時、その結果はコードの戻り値になります。
例1を用いて説明すると、非同期処理Aが成功(fulfilled)すればresolveに戻り値が格納され、失敗(rejected)すればrejectにエラーが格納されます。
fulfilledやrejectedのような状態と結果である戻り値は同じものではないことに注意してください。
言い換えると、「fulfilledやrejectedというステータスが返ってきている = 戻り値がある」ではないのです。
// 例2
Promise.resolve(1).then((value) => {
console.log(value); // => 1
return value * 2;
}).then(value => {
console.log(value); // => 2
return value * 2;
}).then(value => {
console.log(value); // => 4
// 値を返さない場合は undefined を返すのと同じ
}).then(value => {
console.log(value); // => undefined
});
例2を見ていただくと、戻り値の動きがよく理解できるのではないかなと思います。
Promiseチェーン
次にPromiseの重要なメリットの1つである、Promiseチェーンについて見ていきます。
Promiseチェーンとは、一連の非同期処理を連続してthen()を呼び出すことで表現することです。例を見てみましょう。
// 例3
setTimeout(function(){
console.log(1);
setTimeout(function(){
console.log(2);
setTimeout(function(){
console.log(3);
}, 10);
}, 10);
}, 10)
// 例4
Promise.resolve()
.then(() => {
console.log(1);
})
.then(() => {
console.log(2);
})
.then(() => {
console.log(3);
});
※Promise.resolve()はfulfilledなPromiseオブジェクトを作成しています
例3はコールバック関数を使って123を表示させており、例4は同じ処理をPromiseチェーンを用いて表現したものです。各処理をコールバック中で入れ子にする必要がなく、比較すると下のほうが見やすいコードになっていると思います。
Promiseチェーンを簡略化して書いてみると、次のようになっていることが分かります。(例5)
// 例5
Promise.resolve().then().then().then()
では例5を用いて仕組みを考えてみます。
Promise.resolve()はPromiseオブジェクトを返します。最初のthen()は、このPromiseオブジェクトのメソッドとして呼び出しています。ここで注意点を思い出してほしいのですが、Promiseは複数の処理を表現することができません。つまり、最初のthen()を呼び出すことはできても、2つ目、3つ目のthen()は呼び出すことができないのです。
では、どうやってPromiseチェーンを実現しているのでしょうか。それは、最初のthen()が新たにPromiseオブジェクトを返しているのです。
そしてそのPromiseオブジェクトが、2つ目のthen()メソッドを呼び出します。それがthen()の続く限り繰り返されます。そして最後、後に続くthen()が無くなれば、そこで非同期処理のチェーンは終了します。
then()が呼び出され続けるのは、Promiseオブジェクトがfulfilledの場合に限ります。もし、then()の中でエラーが起こり、rejectedになった場合は、次に呼び出されるメソッドはthen()ではなく、catch()になります。(例6)
// 例6
Promise.resolve()
.then(() => {
throw new Error("例外");
})
.then(() => {
// このthenのコールバック関数は呼び出されません
})
.catch((error) => {
console.log(error.message);
});
さらに付け加えれば、メソッドが正常に完了すればfulfilledとなるので、当然catch()が正常に完了すればPromiseオブジェクトの状態はfulfilledとなります。
つまり、例7のさいごのthen()は呼び出されるということです。
// 例7
Promise.resolve()
.then(() => {
throw new Error("例外");
})
.then(() => {
// このthenのコールバック関数は呼び出されません
})
.catch((error) => {
console.log(error.message);
})
.then(() => {
// このthenのコールバック関数は呼び出されます
});
finallyメソッド
ES2018(ES9)では、then()やcatch()のほかにfinally()も定義されています。
Promiseチェーンにfinally()を追加すると、Promiseが完了したときにfinally()に渡したコールバックが呼び出されます。finally()はfulfilled、rejectedどちらの場合でも実行されます。
一般的にはPromiseチェーンの後処理をコールバックに登録します。
// 例8
function examplePromise() {
return new Promise((resolve, reject) => {
// 非同期処理A
});
}
examplePromise().then(()=> {
console.log(resolve);
}).finally(() => {
console.log("処理が終了しました");
});
例8は、Promiseオブジェクトがfulfilledであろうが、rejectedであろうが、finally()は呼び出され「処理が終了しました」と出力されます。
Promiseの並行処理
then()で非同期処理をつなぐことで、逐次的な処理を行うことができます。しかし、いくつかの非同期処理を並行して処理したい場合や、処理の順番は関係なくただ全ての処理をしたいだけの場合もあります。
そのようなときは、Promise.all()を使うことで複数のPromiseをまとめることができます。
Promise.all()
// 例9
function delay(timeoutMs) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(timeoutMs);
}, timeoutMs);
});
}
Promise.all([delay(1), delay(2), delay(3)]).then(values => {
console.log(values); // => [1, 2, 3]
});
例9は簡単なPromise.all()の使い方です。
Promise.all()は引数としてPromiseオブジェクトの配列をうけとり、Promiseオブジェクトを返します。入力として渡したPromiseのどれか1つでもrejectedの場合、返されるPromiseオブジェクトはrejected。全てがfulfilledだった場合fulfilledであり、値は各Promiseの値の配列が渡されます。
Promise.allSettled()
ES2020(ES11)で導入されたPromise.allSettled()は、Promise.all()とすこし異なります。
Promiseの配列を受け取り、Promiseを返します。これはPromise.all()と同じです。異なるのは、Promise.allSettled()の戻り値はrejectedにはならない事です。そして、入力したPromiseが全て完了するまでfulfilledになりません。
// 例10
Promise.allSettled([Promise.resolve(1), Promise.reject(error), 3]).then(results => {
results[0] // => { status: "fulfilled", value: 1}
results[1] // => { status: "rejected", reason: error}
results[2] // => { status: "fulfilled", value: 3}
});
返ってくる配列の要素は、各入力のPromiseに対応する連想配列です。この連想配列は、statusプロパティを持ち、statusにはfulfilledとrejectedが設定されます。そして、それぞれの場合で異なるプロパティを持ち、
fulfilledの場合、valueプロパティを持ち、Promiseを満たした値
rejectedの場合、reasonプロパティを持ち、Promiseのエラー(失敗したときの値)
が設定されます。
Promise.race()
Promise.all()は全てのPromiseがすべて完了するまで待つメソッドでした。しかし、複数の非同期処理を行い、最初に帰ってきた値だけを取得したい場合もあるかもしれません。そんな時はPromise.race()が使えます。
Promise.race()の使い方は、Promise.all()と同じです。Promise.race()が返すPromiseは、入力した配列中のPromiseで最初にfulfilledかrejectedになるものが現れた時に、同じ状態になります。
function delay(timeoutMs) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(timeoutMs);
}, timeoutMs);
});
}
Promise.race([
delay(1),
delay(32),
delay(64),
delay(128)
]).then(value => {
console.log(value); // => 1
});
おわりに
Promiseがどのように動いているのか、そしてどのような使い方ができるのかという部分を整理してみました。
Promiseの状態などややこしい部分が多く、使いこなせるようになるにはとても苦労しそうです。
参考文献
「JavaScript Primer」https://jsprimer.net
「JavaScript 第7版」David Flanagan 著、村上 列 訳
お問合せはお気軽に
SHIFTについて(コーポレートサイト)
SHIFTのサービスについて(サービスサイト)
SHIFTの導入事例
お役立ち資料はこちら
SHIFTの採用情報はこちら
PHOTO:UnsplashのShahadat Rahman