前回のあらすじ
直列処理
async / await 、非同期処理を順番に実行(直列処理)を簡単に書くことができます。
async function sleepSquareFunc(x) { return new Promise((resolve, reject) => { setTimeout(() => { resolve( x * x ); }, 1000); }); } async function sumSerialFunc() { const a = await sleepSquareFunc(2); const b = await sleepSquareFunc(4); const c = await sleepSquareFunc(6); return (a + b + c) * 2; } const sum = serialFunc(); // => sum: 112
上記の例ではa
, b
, c
を順番に求めますが、await
は処理を止めて結果が返されるのを待つので最終的な結果を返すまでに3秒は待つ必要があります。
await
させる関数に関連性があって順番に処理を行う必要があるのであれば問題ないのですが、上記のような関連性が無い非同期処理なら逐一await
で待たせて処理をする必然性がありません。
並行処理
Promise.all
を使う
async function
はPromiseを返す関数なので、並行処理したいものはPromise.all
で取得してしまえばOK。
async function sleepSquareFunc(x) { return new Promise((resolve, reject) => { setTimeout(() => { resolve( x * x ); }, 1000); }); } async function sumParallelFunc() { const a = sleepSquareFunc(2); const b = sleepSquareFunc(4); const c = sleepSquareFunc(6); const [aa, bb, cc] = await Promise.all([a, b, c]); return (aa + bb + cc) * 2; } const sum = serialFunc(); // => sum: 112
非同期処理をループで扱う
ループの際にawait
が処理を止めることを理解しておかないと、意図せず上記の直列処理にしてしまいパフォーマンスの悪いコードになってしまう可能性があります。
ループ内でawait
を使用して直列処理になってしまうパターン
async function calculationByLoop(arg) { let sum = 0 for(let v of arg) { let squareV = await sleepSquareFunc(v); // 直列処理する必要はないが 1000ms 待機してしまう console.log(squareV); sum += squareV * 2; } const endAt = performance.now(); return sum; } const sum = calculationByLoop([2, 4, 6]); // => sum: 112
Array.forEach は何も返さないのでawait
に使えない
Array.prototype.forEach
はundefined
を返すのでasync / awaitで使うことができない
async function calculationForEach(arg) { let sum = 0; const f = await arg.forEach( async (val) => { const squareV = await sleepSquareFunc(val); sum += squareV * 2; }); console.log(f); // => undefined return sum; } const sum = calculationForEach([2, 4, 6]); // => sum: 0
forEach
のループが終わるまで待機されることがないので結果を待つ前にsumが返される。
Array.reduce はreturnする値のawaitの付け方次第で並列処理のように扱われるっぽい
配列の値に手を加えて合算するような場合であれば、Array.prototype.reduce
は値を返すので、内部のコールバックにasync
, await
を使えば、次のループに引数としてPromiseなオブジェクトが渡り並行処理のように処理できるようです。(試してみるまでてっきり直列処理になるものだと思っていました。)
async function calculationByReduce(arg) { const sum = await arg.reduce( async (x, add) => { const a = await sleepSquareFunc(add); console.log('reduce', x, a); // return 値にawait を付けないと、待たずに次のループに入ってしまう return await x + a * 2; }, 0); return sum; }; const sum = calculationByReduce([2, 4, 6]); // reduce 0 4 // reduce Promise { 8 } 16 // reduce Promise { 40 } 36 // => sum: 112
reduce
ループに渡される引数がPromise
オブジェクトになり値が戻されるまでプレースホルダーのように計算結果を待ちつつも、ループ処理そのものはawait
せずに先に進むので結果的に並行処理のようになっているという印象です。
なので、reduce
内でreturn
する値にどうawait
をつけるかによっては直列処理のようになったり、値が返されるまで待たないものになったりする可能性もありそうです。
Array.mapで配列を展開してPromise.allに渡す
Promiseを返す処理にArray.prototype.map
で展開してしまい、Promise.all
に渡してしまえば、それぞれの非同期処理を並行処理することができます。
async function calculationParallel(arg) { const all = await Promise.all( arg.map( async (val) => { const squareV = await sleepSquareFunc(val); return squareV * 2; } ) ); const sum = all.reduce((x, y) => x + y); return sum; }; const sum = calculationParallel([2, 4, 6]); // => sum: 112
取得される値が配列になり、そこからループで処理などが必要になるので処理的にはreduce
より多くなると思いますが、
個人的にPromise.all
にしたほうが見通しは良いかなという印象です。
ただしPromise.all
は渡された処理の1つでもreject
になると、その場でreject
として返されてしまうので、並行処理して取得できた値だけでゴニョるような用途の場合は呼び出す関数がreject
を返さないようにする工夫が必要になりそうです。
reject
含め全ての処理が終わるまで並行処理で待つ関数が欲しい。
おまけ
アロー関数でasync function
を書く時はこんな感じ
const functionName = async () => { ... }
async
はあくまで function
の前につけるキーワード。
[参考]
- Promise.all() - JavaScript | MDN
- Array.prototype.forEach() - JavaScript | MDN
- Array.prototype.reduce() - JavaScript | MDN
- Array.prototype.map() - JavaScript | MDN
わかるよ! 電流・電磁石 小学生の理科 -直列並列、抵抗、発熱、電流と磁力線- (DVDビデオ) (わかるよ! シリーズ)
- 発売日: 2013/07/10
- メディア: 単行本