非同期処理を調べると必ず、タイトルのこの二つが登場してくると思いますし、実際の現場でも割と混在しているのではないでしょうか。
そして意外と使い分けに悩む・・・
なので今回は改めてasync/awaitとPromiseについて調べてみました。
とりあえず結論
async/awaitはPromiseを使いやすくするために生まれた文法ですので、
基本的にはasync/awaitを使ったらいいと思います。
async/awaitとPromiseって何者?
一応簡単に触れておくと、jsでの非同期処理を制御してくれるやつですね。
そもそもjsでは色々あって非同期的に動くことが多いです。
この色々の箇所を解説すると長いので細かい話は割愛しますが、jsはWebAPIなどで外部の処理を駆使して、細かい処理を実現したりしています。
(jsの管轄外の外部で処理が動くので、いつ終わるかわからず待ってられないようなイメージ。待ってられないからjsは非同期的にどんどん処理を外部に投げながら自分は進んでいくイメージ)
まぁ細かい話は、別で調べていただけると助かります。
で、async/awaitとPromiseは、そのjsの非同期的な処理を同期的に動かすための機能です。
非同期に動くものを同期待ちさせるための機能って言った方がわかりやすいですかね。
もっと具体的に言うと、処理Aが終わってから処理Bをして欲しいみたいな時に、
処理Aが終わるまで待って、終わった合図が出てから次の処理やってね★って指示を出すことができます。
なにがasync/awaitはPromiseの進化なの。
とりあえず、今回は以下の処理を制御していきます。
一応補足なんですが、例で出てくるmethodは色々あって非同期に動くと思ってください。
(jsには非同期になる処理とならない処理があります。)
function main() {
methodA() // API通信で10秒くらいかかる処理
methodB() // API通信で20秒くらいかかる処理
methodC() // API通信で15秒くらいかかる処理
}
main()
// 3つの処理が同時に実行されて、約20秒で全て実行が完了する
この処理は実行されると3つの処理が合わせて約20秒で終わります。
非同期的な処理なので同時に実行されるからです。
この非同期的な動きを制御したい時、すなわち「methodBが終わってからでないとmethodCを実行してはいけない」ような状況の場合に、今回のPromiseやらasync/awaitを使います。
今のままだと、全部同時にスタートするのでmethodA→methodC→methodBの順に終了しちゃいます。
ではA→B→Cの順になるように制御してみます。
Promise使ってみる
では、Promiseを使ってみます。
function main() {
new Promise(resolve => {
methodA() // 10秒の処理
resolve()
}).then(() => {
return new Promise(resolve => {
methodB() // 20秒の処理
resolve()
})
}).then(() => {
return new Promise(resolve => {
methodC() // 15秒の処理
resolve()
})
})
}
main()
// methodがA→B→Cの順に実行される。処理はメソッド一つずつ行うので45秒かかる
これでA→B→Cで実行されます。
Promiseがどういう働きをしているのかざっくりと説明すると、
まずPromiseがreturnされたり、newされたりするとjsは待機します。
そしてPromiseの引数で書かれる処理の中でresolve()が実行されるとPromiseは「次の処理実行してもらっていいっすよ」という合図を出します。
その合図を受けるとthenで繋いでいる処理を実行する。という流れになってます。
今回の例だと、「Promiseだ!待機!resolve()だ!thenの処理をするぞ!Promiseだ!待機!・・・・」みたいになってるのがわかると思います。
(ちなみにresolve()の代わりにreject()を呼び出すか、エラーが発生すると「この処理失敗してます・・・」という合図をPromiseは出します。その場合はthenと同じ要領で書けるcatchで処理を書いておけばそちらが実行されます。以下例)
new Promise(resolve => {
const a = null
if (a == null) {
// aはnullなので必ずreject()される
reject()
}
resolve()
}).catch(() => {
console.log("エラー")
})
// コンソールに「エラー」というメッセージが表示される
ここまで書けばわかると思うんですが、Promiseで書くとちょっと見づらいと思います。
今回は割とシンプルにまとめてますが、一つ一つの処理の行数が5,6行になってくるともうなんのこっちゃわからなくなります。
難しく言うとネスト地獄になりやすいです。
で、本題ですが、
この見にくさをasync/awaitだと結構すっきりさせることができます。
そもそもasync/awaitはこの見づらい非同期制御をぱっと見同期的な処理に見える書き方をできるようにしようぜ!と発明されているっぽいです。
async/await使ってみる
async/await。正しい読みは「エイシンク/アウェイト」らしいです。
では、さっきの処理をasync/awaitで書くと、
// とりあえず全メソッドasyncにします。
// asyncをつけて関数を定義することで返り値がPromiseに含まれた形で返却される。
// 返却値なしだとPromise<void>、numberが返るとPromise<number>みたいな
async function methodA() {
// 謎の10秒処理
}
async function methodB() {
// 謎の20秒処理
}
async function methodC() {
// 謎の15秒処理
}
// awaitで呼び出せば、その関数が終わるまで待ってくれる
// asyncメソッドの中でしか、awaitが使えないのでasyncで定義する
async function main() {
await methodA()
await methodB()
await methodC()
}
main()
// methodA→B→Cの順に実行されて45秒かかる
だいぶ見やすくなったんじゃないでしょうか。
asyncとawaitをつけるとどうなってるのかを簡単に説明します。
まず非同期制御に関わる関数をasyncで定義します。
asyncをつけると、大きく二つ意味があって、
そのasync付きの関数は非同期処理だよっていう宣言と共にPromiseを返却するようになります。
そしてreturnのタイミングでresolve()を勝手にしてくれます。
(具体的な動きとしては、もしasync関数の返却値がnumberだったらresolve(numberの値)を実行してくれる感じですね。)
もう一つはasyncで定義された関数内ではawaitの宣言と共に関数を呼び出すことができます。
awaitで呼べる関数は非同期処理(Promiseを返却する関数、asyncで定義された関数)のみです。
awaitはawaitで宣言されている関数が終了するまで待ちます。
具体的にはPromiseが返却されるので、resolve()されるのを待ってるという感じですね。
ちなみに細かい話をすると、async/awaitで処理を書いているとtry~catchで囲んで例外処理を書けますし、Promiseと同じように.catchを使ってのハンドリングもできます。(もちろん.thenで次に実行させたい処理を書いたりもできます。)
async/awaitはforEachじゃ使えない
ここまで書いてきた内容だと、じゃあasync/awaitだけ何も考えず使えばいいじゃんってことになると思いますが、少し弱点があります。
forEach内ではasync/await使えないってやつですね。
// 適当な配列
const aaaList = [1, 2, 3]
// 以下の使い方はできない
aaaList.forEach(async () => {await methodB()}
そもそもforEachはasync/awaitにそもそも対応した作りがされていないそうです。
ただ、for…ofの書き方であれば問題なく使えますので、基本的にはそちらを代用してもらえれば問題ないかなと思います。
あとは、for…ofが嫌だよって人がもしいるなら、Promise.allで代用もできます(直感的に書き換えにくいですし、だいぶ見栄えも違いますし、処理が並列に動くところも全然違うので、代用というのもなかなか微妙かもです)。
Promise.allは引数にPromiseを返す関数の配列を渡せば、配列の関数を全て並列に実行してくれます。
そして、配列内の全ての関数のPromiseがresolve()になると次の処理へ進んでくれます。
async function methodA {
// 謎の処理
}
async function methodB {
// 謎の処理
}
async function methodC {
// 謎の処理
}
function main() {
Promise.all([methodA, methodB, methodC])
console.log("終わり")
}
main()
// methodA〜Cが並列に実行され、すべて完了するとコンソールに「終わり」と出力される
これは余談ですが、Promise.allと似た処理にPromise.raceっていうのもあります。
こっちはPromise.allと違い、並列に実行している関数のどれか一つでも終われば次の処理にいきます。
あんまり使わないかもですが、よかったら頭の片隅にどうぞ。
終わり
Promiseとasync/awaitの使い分けで迷われた方向けに記事を書いてみました。
結論はasync/await使ってたらいいと思います。(forEachだけ気をつけてください)
非同期処理ってむずかしいですね。
参考
・https://yumegori.com/javascript-async-await