asyncとawait付ければ非同期処理で使うんだよなーぐらいの認識で業務のコードを読んでいたけど、自分で実装するとなって改めて勉強するとたくさん学びがあったのでまとめておきます。
色々調べて試したつもりですが、違っていたり、もっと良い書き方があれば是非ご教授ください…!
同期処理と非同期処理
簡単に復習します。
同期処理
ひとつひとつ処理が終わってから次の処理に進む。
非同期処理
ある処理(APIへのリクエスト・時間が掛かる処理など)が終わるのを待たずに、いったん次の処理を継続する。
例えば、データを取得してくるAPIリクエストの間に、そのデータは無くてもできる他の処理は進めておこうみたいなイメージです。
Promise
「処理が終わったら結果を返します!」という約束代わりに一旦渡しておくのがPromise。
実体はオブジェクトです。
Promiseの持つメソッドや後述するasync・awaitを使うと、非同期処理の結果を待ってから次の処理を実行したり、複数の非同期処理を順番を決めて実行することができます。
(非同期処理は終了を待たずに他の処理を進めることが出来るというものですが、実際にコードを書いていると非同期処理の終了を待ちたいケースが断然多い気がします。)
Promise
Promiseオブジェクトが持つthenメソッドやcatctメソッドを使って、Promiseオブジェクトの非同期処理が終わってから実行したい処理を書くことができます。
非同期処理を行う関数からPromiseオブジェクトを返し、Promiseオブジェクトに.then()や.catch()を繋げて書きます。
基本の使い方
Promiseの使い方は以下のようなイメージです。(ちなみにasyncは非同期という意味です。)
const promise = asyncFunc()
// promise:Promiseオブジェクト
// asyncFunc():何らかの非同期処理。Promiseオブジェクトを返す。
promise
.then((response) => console.log(response)) //正常におわったとき
.catch((error) => console.log(erroe)) //エラーが発生したとき
.finally(() => console.log('Done!')) //いずれの場合も実行する
then()はPromiseオブジェクトが返すレスポンスを、catch()は発生したエラーを受け取って使うことができます。finally()はいずれの場合も実行されます。
よってPromiseオブジェクトの処理が正常に終わればthen()とcatch()が、何かエラーが起きればcatch()とfinally()が実行されることになります。
then, catch, finally は使いたいものだけ使えばOKです。
知らない間にPromiseを使っていた
業務のコードでaxiosが使われており、axios.get('/url').then()
といったコードを見て、私は通信後に行いたい処理をthen()に書くんだな~と何となく使っていました。
改めて勉強してみると、 axios.get('/url')
の返り値はPromiseオブジェクトであるため、 axios.get('/url')
が返すPromiseオブジェクトに対してthenメソッドを繋げて書いているということだったのです。(本当に激浅い理解でコードを読んでいたと気付きました…)
Promiseを勉強しようと色んな記事を読むと、まず new Promise
をしてPromiseオブジェクトを生成したり resolve
やreject
の話が出てきて難しく感じましたが、 new Promise
のように明記していなくても実はこうやって何気なくPromiseを使っていたのだと知りました。
複数の非同期処理を順番に行う
Promiseの使い方の話に戻ります。
Promiseオブジェクトに対してthenを以下のように複数つなげると、複数の非同期処理を一つずつ順番に行うことができます。
せっかくなので具体的なイメージが沸きやすいよう、axiosを例にとって説明します。(もろもろのaxiosを使う準備は出来ている前提とします)
以下のように書くと、 axios.get('/a')
のレスポンスが返って来るのを待ったうえで axios.get('/b')
を行い、そのレスポンスが返って来てから console.log('Done!')
を実行します。
axios
.get('/a')
.then(() => axios.get('/b')) //thenの中でPromiseオブジェクトを返す
.then(() => console.log('Done!')) //axios.get('/a')の処理が終わったら実行
.catch()
.finally()
仮に以下のように書いたとするとaxios.get('/b')
の非同期処理の終了を待たずに console.log('Done!')
が行われてしまうというわけです。
axios
.get('/a')
.then(() => axios.get('/b'))
.catch()
.finally()
console.log('Done!') //axios.get('/a')の終了を待たずして実行される
以下のように、いくつでも繋げて書くことができます。
axios.get('/a')
.then(() => axios.get('/b')) //thenの中でPromiseオブジェクトを返す
.then(() => axios.get('/c'))
.then(() => asyncFunc()) //asyncFunc()はPromiseオブジェクトを返す関数
.catch()
.finally()
thenを複数つなげるときの注意点
ここで注意点が2つあります。
① then()の連結により複数の非同期処理を順番に行いたい場合、then()の中でPromiseを使うだけではなくPromiseオブジェクトをreturnしなければいけません。
② 途中でエラーが発生した場合はそれ以降のthen()は飛ばされて、catch()とfinally()の処理が実行されます。
例えばPromiseオブジェクトを返すasyncFunc()という関数があるとします。
アロー関数の書き方のちょっとした違いで、Promiseオブジェクトをreturnしていないとその非同期処理の終了を待たずして次の処理が進んでしまうので、注意が必要です。
//Promiseオブジェクトをreturnできている例
.then(() => asynccFunc())
//Promiseオブジェクトをreturnできていない例
.then(() => {
console.log('then')
asyncFunc()
})
//処理が複数行の場合はreturnしなければいけない
.then(() => {
console.log('then')
return asyncFunc()
})
async・await
先述のようにPromiseオブジェクトに対してthenをいくつも繋げて書いてもよいのですが、これをもっと簡潔に見やすく書くことができるのがasync・awaitです。
基本の使い方
async
- 関数にasyncを付けると「この関数は非同期関数だよ」と意味づけ、関数を実行するとPromiseを生成して返してくれる。
- Promiseオブジェクトが持つthen, catch, finally を使える。
await
- asyncが付けられた関数内でのみ使用可能。
- Promiseオブジェクトの前にawaitをつけると、thenメソッドのようにPromiseの結果が返ってくるまで待ってくれる。
複数の非同期処理を順番に行う
コードを見た方が、async・awaitは直感的に理解しやすいことが分かると思います。
async・awaitが便利なのは例えばこんなときです。
asyncFunc().finally(() => console.log('done'))
const asyncFunc = async () => {
await axios.get('/get').catch(() => console.log('getのエラー'))
await axios.post('/post').catch(() => console.log('postのエラー'))
}
ここでのポイントは3つです。
① asyncFunc()に async
を付けることで、この関数の返り値がPromiseオブジェクトになる。
② asyncFunc()の中には非同期処理が2つあり、両方に await
が付いているのでひとつずつ終了を待ってから次の処理を実行する。
③ asyncFunc()の処理結果が全て出た時点でfinally()を実行する。
業務で詰まった例をご紹介します。(本当はもっと色んな処理をしていますが、超簡略版です。)
まずは最初に書いて上手く行かなかったコードがこちら。
asyncFunc().finally(() => console.log('done'))
const asyncFunc = async () => {
await axios
.get('/get')
.then(() => {
axios
.post('/post')
.then((res) => console.log(res))
.catch(() => console.log('postのエラー')
})
.catch(() => console.log('getのエラー'))
}
Promiseでthenをつなげる時の注意点で挙げた通り、thenの中でPromiseオブジェクトをreturnできていないので、これでは axios.post('/post')
の終了を待たずしてasyncFunt()につなげたfinally()が実行されてしまいます。
またNGではありませんが、ネスト構造のせいで get('/get')
に対するcatchが最後に来ているのも分かり辛いですね。
これを以下のように改善しました。
awaitを使うと上から順に処理するというのがパッと見でも分かりやすくなります。
asyncFunc().finally(() => console.log('done'))
const asyncFunc = async () => {
await axios.get('/get').catch(() => console.log('getのエラー'))
await axios.post('/post')
.then((res) => console.log(res))
.catch(() => console.log('postのエラー')
}
ひとつ注意したいのは、 axios.get('/get')
の時点でエラーを拾ったとしても次の処理は実行されるということです。
もしエラー時の処理が共通なのであれば、thenを複数つなげるのも一つの方法かと思います。
asyncFunc().finally(() => console.log('done'))
const asyncFunc = async () => {
axios
.get('/get')
.then(() => axios.post('/post'))
.then((res) => console.log(res))
.catch(() => console.log('共通のエラー処理'))
}
このように書いた場合はどの段階でエラーが発生しても .catch(() => console.log('共通のエラー処理'))
と finally()
が実行されます。
途中でエラーが発生した場合は、その後のthenの処理は行われません。
注意したいこと
順番は問わない複数の非同期処理
複数の非同期処理を順番に行う方法について、thenをつなげて書くパターン・awaitを使うパターンを先述しました。
ですが非同期処理の結果が相互に影響しない場合は、ひとつずつ終了を待つ分パフォーマンスが落ちてしまうので、次にように複数のリクエストが並列して処理されるようにします。
良くない例
良くない例から見ていきます。
※以下で使うfetchメソッドは、非同期通信でリクエストを発行してPromiseを返すJavaScriptの組み込み関数です。
// Promiseオブジェクトを返す関数
const getData = (url) => fetch(url)
const asyncFunc = async () => {
await getData('https://hoge')
await getData('https://fuga')
console.log(res1)
console.log(res2)
}
このコードは問題なく動作しますが、res1に結果が代入されるまでasynccFunc内の処理を止めてしまいます。
良い例(Promise.all)
すべての非同期処理が成功した場合にコールバックを実行するPromise.allメソッドを使うと、複数のリクエストを並行して処理することができます。
// Promiseオブジェクトを返す関数
const getData = (url) => fetch(url)
const asyncFunc = async () => {
const urls = ['https://hoge', 'https://fuga'];
console.log(await Promise.all(urls.map(get)));
}
Promise.allメソッドは、引数に監視するPromiseオブジェクト(もしくはPromiseオブジェクトを返す関数)を配列で渡して使います。
Promise.allにthenなどをつなげて使うこともできます。
// 定義元
const asyncFunc = (urls) => Promise.all(urls.map(get))
// 呼び出し側
const urls = ['https://hoge', 'https://fuga']
asyncFunc(urls)
.then((res) => console.log(res))
.catch((err) => console.log(err))
ポイントとして、Promise.allに渡したPromiseオブジェクトの中でひとつでもエラーが発生した場合は、catchメソッドの失敗コールバックが呼び出されます。
Promise.race
Promise.allは順不同で全ての非同期処理の終了を待つものでした。
複数の非同期処理のうちどれか一つが終了したら良い場合のためには、Promise.raceというものが用意されています。
実装方法はPromise.allの部分をPromise.raceに変えるだけです。
ポイントは2つです。
① 複数のPromiseオブジェクトのうち、最初に解決したPromiseオブジェクトのコールバックが実行される。
② Promiseの結果がresolveになるものとrejectになるものが混ざっていても、最初に処理が終わったPromiseオブジェクトの結果次第に沿ってコールバックが実行される。
さいごに
色んな書き方を試して上手く行かなくて調べての繰り返しで、たくさん勉強になったので記事にしておきました。
ただまだPromiseを勉強していると出てくる new Promise
が必要な場面に出会ったことが無いし、時と場面によってより良い書き方もあるはずなので、新しい学びがあったら書き足していこうと思います。