ReactなどでAPIを叩いた時に返ってくるPromiseについてよくわからなかったので、調べて自分なりに解釈してみた。Promiseは概念を理解するのが結構難しく自分はまだ全てを完全に掴めたわけではない。
ある程度大筋を理解したので、アウトプットしてみる。全く知らない人に参考になればうれしい。
テスト用のReactのプロジェクトがあるので、自分はそこでAPIを叩いたりしてます。(JsonPlaceholderを叩いてます。)
自身でお好きな環境を用意していただければと思います。
Promiseとは?
そもそもPromiseとはなんだろう。
公式によれば非同期処理の完了(もしくは失敗)の結果および値を表すオブジェクトだ。
つまり、非同期処理を行なってその結果返ってくるオブジェクトがPromiseだ。
Promiseが持つ3つの状態
Promiseは以下の3つの状態を持つ。
- pending(初期状態)
- fulfilled(処理が成功して完了)
- rejected(処理が失敗)
の3つ。この概念はしっかり押さえておこう。
初期状態はpendingで、この状態から処理が成功してfullfilledになったり、失敗してrejectedになったりする。
pendingからfulfilled、rejectedのどちらに状態が変更しても、settledといって決定したという扱いになる。
下の表がPromiseの一連の流れを表したものなので、これを適宜見ながら理解していこう。
thenメソッド
Promiseオブジェクトはthenメソッドを持つ。Promiseを特に理解していなくても、fetch関数やaxiosを使っていれば、特に意識せず使っているのではないだろうか?
//axiosを使った例
axios
.get("https://jsonplaceholder.typicode.com/posts")
.then((response) => console.log(response.data))
.catch((error) => console.log(error));
上のaxiosメソッドのgetの後に、thenと書いてその中で処理を書くということをReactなどのJavaScriptフレームワークを使ったことがある人なら一度は経験があるのではないだろうか?
ここでthenについて見ていく前に押さえておきたいことが、getで返ってくるものはPromiseオブジェクトだということだ。なぜなら、thenメソッドはPromiseオブジェクトが持つメソッドなので、thenが使えるということは、その前はPromiseオブジェクトということになる。
thenメソッドが持つ2つの引数
上を押さえたら、再度下の画像を見ながらthenメソッドが持つ2つの引数について見ていこう。
thenメソッドは2つの引数を持つ。第一引数がonFulfilledで、第二引数がonRejectedだ。ここには、どちらも関数を入れる。また、どちらの引数も省略可能である。
(onFulfilledやonRejectedは仮引数の名前なので、ここには自分で行いたい処理を定義した関数を入れます。)
Promiseの状態がfulfilledになった時にthenの第一引数に渡した関数、rejectedになった時にthenの第二引数に渡した関数が実行される。
そして、thenに渡したそれぞれの関数は引数を受け取り、fullfilledになった時の値や、rejectedになった時のエラーオブジェクトを受け取る。
Promiseの状態がfulfilledになっているということは、何らかの値を持っているし、rejectedになっているということは、エラーの理由を持っている。それらの値や理由がthenメソッドに渡した関数の引数に渡されるというわけだ。
また、Promiseの状態がpendingから変化すればどちらにしろsettledになる。settledになった時に発動したい関数をthenメソッドで登録することができると覚えておこう。
ここで登録される関数のことを、いわゆるコールバック関数と呼ぶ。コールバックは「〇〇が終わったら後で呼んでね!」という意味だ。
axiosで実際にどうなるか以下のコードを見ていこう。
//axiosを使った例
axios
.get("https://jsonplaceholder.typicode.com/posts")
.then((response) => console.log(response.data),(error) => console.log(error))
thenの第一引数には以下の関数を入れた。パッとみた感じ関数に見えないかもしれないが、アロー関数で記述されてある。
(response) => console.log(response.data)
もちろん、以下のようにfunctionで定義した関数を入れてもオーケーだが、冗長になるのでアロー関数で書かれていることの方が多いのではないだろうか。
function (response) {
return console.log(response.data);
},
この(response) => console.log(response.data)
のresponse部分にPromiseでfulfilledになった時の値が入ってくる。
「Promiseでfulfilledになった時の値」という表現は、正確に言うと後述するPromiseがresolveされた結果のことだ。
難しければ非同期処理に成功した後の値が入ってくるといってもいい。
ここでは、axiosがfulfilledになったらresponseオブジェクトが返ってくる。response.dataでresponseオブジェクトのdataプロパティにアクセスして中身を確認すると、実際の中身の部分を取得できる。今回はJsonPlaceholderのpostsを叩いてるのでデータ100件が取得できる。
第二引数部分の(error) => console.log(error)
も、アロー関数で書いており、error
の部分に、Promiseでrejectになった時の値が入ってくる。概ね、rejectされたときはerrorオブジェクトが入ってくるみたいなので、ここではエラーの場合errorオブジェクトが返ってくる。
catchメソッドについて
thenメソッドでは、上記のように2つのコールバック関数を引数にとることができた。第一引数では成功した時に発動する関数、第二引数では失敗した時の関数だ。ただ、上のような書き方は少なく、だいたいが以下のようにcatchメソッドを使ってエラーハンドリングをしているのではないだろうか?
//axiosを使った例
axios
.get("https://jsonplaceholder.typicode.com/posts")
.then((response) => console.log(response.data))
.catch((error) => console.log(error));
catchはthenメソッドの第一引数がnullで第二引数だけ登録されているものと全く一緒だ。then(null, 失敗したときに発動する関数)
とcatch(失敗したときに発動する関数)
は全く一緒。
thenの第二引数に書くよりも、catchメソッドに書いていった方がコードが見やすいので、catchメソッドで書いていくほうがおすすめだ。
thenメソッドの返り値とメソッドチェーン
thenはもちろんメソッドなので返り値がある。thenメソッドの返り値は新しいPromiseオブジェクトだ。
thenメソッドがPromiseを返すということは、さらにそこにthenメソッドやcatchメソッドを続けて書くことができる。
これをメソッドチェーンや連鎖という。
例を見ていこう。
axios
.get("https://jsonplaceholder.typicode.com/posts")
.then((response) => response)
.then((response) => console.log(response.data))
.catch();
上の3行目でresponseを受け取りそのままresponseオブジェクトを返して、4行目でresponseからdataをconsole.logで表示している。
3行目に.then((response) => console.log(response.data))
と書けばもちろんそれだけで上と同じことはできるのだが、注目して欲しいのは4行目の引数responseに3行目で返した値が入ってくるということだ。
thenメソッドがPromiseを返すということは、そのPromiseは最初に紹介した3つの状態を持っていることになる。thenの第一引数の処理が成功するとthenで返されるPromiseはfulfilled状態になり、次のthenにその処理の結果が渡されるので4行目で3行目で返した値が渡されることになる。
簡潔に言うとthenの中で返した値が次のthenの引数に渡されるということだ。
これがわかると例えば以下のコードを実行すると、、、
(1行目から3行目は人為的にPromiseを生成してます。後述するので、今は文字列として"ラーメン"が返されるPromiseだと見といてください。)
const testPromise = new Promise((resolve, reject) => {
resolve("ラーメン");
});
testPromise
.then((value) => value + "つけ麺")
.then((value) => value + "ぼくイケメン")
.then((value) => console.log(value));
以下のように出力される。
注意点としては、値をreturnしないと次に渡らないということだ。
例えば上のコードを以下のように変更してみる。
const testPromise = new Promise((resolve, reject) => {
resolve("ラーメン");
});
testPromise
.then((value) => console.log(value + "つけ麺"))
.then((value) => value + "ぼくイケメン")
.then((value) => console.log(value));
5行目のreturnの部分をconsole.logに変更した。上のコードを実行すると以下のように出力される。
5行目ではもちろん、「ラーメンつけ麺」と表示されたが、7行目の最終結果は「undefinedぼくイケメン」になっている。
これはつまり、「ラーメンつけ麺」というvalueが次のvalueとして、渡されていないことになる。なぜならconsole.logを使うことによって値を返さなくなったからだ。
このように値を返さなかった場合はundefinedと表示されるのでこれも頭の片隅に入れておこう。
また、Promiseがrejectや何かエラーを起こすと、thenメソッドの第一引数の関数に値は渡らず、連鎖を辿ってcatchメソッドが呼ばれるのでこちらも押さえておこう。(thenの第二引数に関数を用意していればもちろんそちらが呼ばれます。)
ここでは、説明の便宜上catchメソッドを書いていないところもあるが、エラーを検知させるために必ずcatchメソッドは用意しておこう。
thenの中でPromiseを返した場合は?
thenメソッドが新しくPromiseを返すことはわかったが、thenの中の関数でPromiseを返した場合はどうなるのだろうか?
結論から言うと、thenの中で返したPromiseオブジェクトの結果が次のthenに渡される。
上は公式から持ってきた文だが噛み砕くと、thenが返すPromiseの値は、thenの中の関数(ハンドラー)で返したPromiseに依存しますよということだ。
testPromise("ramen")
.then((value) => {
return new Promise((resolve, reject) => {
resolve(value + "つけ麺");
});
})
.then((value) => value + "ぼくイケメン")
.then((value) => console.log(value))
.catch((error) => console.log(error));
先ほどまでのコードの2~6行目をPromiseに変更しても以下のように出力される。
2~6行目のPromiseでresolveした値が次のvalueに渡されているわけだ。
2~6行目でrejectさせると当然エラーが返される。
エラーが返されるのでthenの第二引数に関数を用意してない限り、catchメソッドまで連鎖を辿る。
.then((value) => {
return new Promise((resolve, reject) => {
// resolve(value + "つけ麺");
reject(new Error("エラーを発生させる"));
});
})
thenの中でPromiseを返してもそのPromiseの結果が次に渡させれるということを押さえておこう。
つまりは、thenで実行されるのが非同期処理でも、非同期処理でなくてもPromiseが返されるということだ。
Promiseオブジェクトを作成する
Promiseオブジェクトは自身で作成することもできる。ここではそのやり方を見ていこう。
const myFirstPromise = new Promise((resolve, reject) => {
// 次のどちらかを呼び出す非同期処理を行います。
//
// resolve(someValue) // 履行
// または
// reject("failure reason") // 拒否
});
上のコードが、Promiseを作成する時の構文だ。(公式より引用。)
基本的には、new Promise()
と書いて引数として関数を受け取る。その関数では、さらにresolve、rejectを受け取り、resolveを使ってPromiseの状態をfulfilledにrejectを使ってPromiseの状態をrejectにする。
自分は、ここら辺で頭がごっちゃになったので一度整理しておこう。
- Promiseの状態は、pending、fulfilled、rejectedの3つ。
- 上の状態を変化させるには、Promise内でresolveやrejectを用意して、実行する。
- resolveすれば、Promiseの状態はfulfilledに。rejectすればPromiseの状態はrejectになる。
Promiseの状態とPromiseの状態を変化させるものの区別をつけておこう。
例えば、以下のコードだとfulfilledになるPromiseオブジェクトが生成される。
const testPromise = new Promise((resolve, reject) => {
resolve("ラーメン");
});
以下のコードだったら、rejectedになるPromiseオブジェクトが生成される。
const testPromise = new Promise((resolve, reject) => {
reject(new Error("error"));
});
さらに以下のコードで、引数によってresolveやrejectが実行されるPromiseオブジェクトを作成できる。
const testPromise = (ramen) => {
return new Promise((resolve, reject) => {
if (ramen === "ramen") {
resolve("ラーメン");
} else {
reject(new Error("ラーメンではない"));
}
});
};
3から7行目で引数に渡した値が"ramen"だったらresolveして、それ以外だったらrejectを実行するPromiseだ。
これを前の章で実行したメソッドチェーンと組み合わせてみよう。
testPromise("ramen")
.then((value) => value + "つけ麺")
.then((value) => value + "ぼくイケメン")
.then((value) => console.log(value))
.catch((error) => console.log(error));
上は引数で"ramen"を渡したので以下のように出力されて
testPromise("ああああああ")
.then((value) => value + "つけ麺")
.then((value) => value + "ぼくイケメン")
.then((value) => console.log(value))
.catch((error) => console.log(error));
上は引数に"ramen"以外を渡したので以下のようにエラーオブジェクトが出力される。
これで何となくPromiseオブジェクトの作り方がわかったのではないだろうか?
多くの非同期関数では上のように内部でresolveやrejectをしていることを押さえておこう。
resolveとrejectという名前は別に何でもよい?
多くの記事や公式ではnew Promise(resolve, reject)
とresolveとrejectと書いているがここの名前は正直なんでもOKだ。new Promise(nandemo,iiyo)
としてnandemo()
とかiiyo()
とか書いても動く。ただ、多くの記事や公式と名前を統一した方がわかりやすいので名前を合わせている。この2つの引数を合わせてexecuterと呼ぶので豆知識として覚えておこう。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise#%E5%BC%95%E6%95%B0
まとめ
- Promiseは非同期処理の完了(もしくは失敗)の結果および値を表すオブジェクト
- Promiseはpending(初期状態)、fulfilled(処理が成功して完了)、rejected(処理が失敗)の3つの状態を持つ。
- Promiseオブジェクトはthenメソッドを持ち、Promiseの状態がfulfilledになった時に発動する関数とrejectedになった時に発動する関数をセットすることができる。
- thenメソッドでセットされた関数はそれぞれ引数を受け取り、前のPromiseで返された値を持つ。(値やエラーオブジェクト)
- catchメソッドは
then(null,失敗した時に発動する関数)
と一緒で、Promiseの状態がrejectedになった時に発動する処理をセットする。 - thenは新しいPromiseを返すので、さらにthenやcatchをつなげることができる。これをメソッドチェーンや連鎖という。
- thenの中でPromiseを返してもOK。その場合、そのPromiseの結果が次のthenに渡される。
- Promiseは
new Promise( )
で作成できる。引数には、「resolveやrejectといった関数を引数に取る関数」を入れる。 - new Promise内でresolveすると、PromiseはfulfilledになりrejectするとPromiseはrejectedになる。
まとめが結構長くなったが、とりあえずこれで自分は、ある程度Promiseの理解を深められた。
まだ深く見ていくこともできると思うがキリがなさそうなので一度ここで終える。書いた方がいい項目があれば適宜編集していきたい。(async awaitとかね)
Promiseが全くわからない状態からちょっと理解できたくらいまで進めばうれしい限りである。
この後に公式のPromiseのリファレンスを読むとさらに理解が深まると思うのでチャレンジできる人はやってみてください。