JavaScript の Array には forEach
や map
や filter
などの便利な高階関数が用意されている。これらは非常に便利なのだが、引数の関数(callback)内で非同期処理を行おうとすると途端に複雑になる。これらの高階関数内での非同期処理の利用について動作を確認したい。
準備
動作確認の準備として非同期の関数 echo
を用意する。
const echo = value => new Promise(resolve => setTimeout(resolve, 1000, value)) |
引数の値を 1 秒後に返すという関数で、以下のような動作をする。
echo('test').then(console.log) |
また、高階関数のレシーバとして配列を作る。
const array = [0, 10, 20, 30, 40, 50, 60] |
forEach
まずはそのまま回して上手く動かないことを確認する。
array.forEach(i => echo(i).then(console.log)) |
1 秒待機して全部一気に表示された
0 |
forEach で async/await
async/await しても結果は同じだった。
array.forEach(async i => { |
async/await を、Array.prototype.forEach で使う際の注意点、という話 - Qiita によると、forEach の callback.call
に await が付いていないからそもそもダメで、for を使いなさいとのこと。
forEach に reduce を使う
変則的な方法として、forEach の代替として reduce を使う。
array.reduce(async (promise, i) => { |
これだと目論見通り、1 秒毎に console.log が実行される。
また、async/await を排除した以下の書き方でも成功する。
array.reduce((promise, i) => { |
forEach の代わりに for…of を使う
安牌の解決方法、変に reduce とか使うより素直で分かりやすい。
;(async () => { |
map
map では全部の要素を 2 倍して出力してみる。特に意味は無いけど…
例によってそのまま動かすと正しい結果が得られず、全て NaN になってしまう。そして表示してから 1 秒待機してしまう。
console.log(array.map(item => echo(item) * 2)) |
2 倍しない場合は Promise { <pending> }
の配列が返ってくるので、これを 2 倍しようとして NaN になってしまうのだろう。
map で async/await
map の callback を async/await で装飾するだけだとやはり Promise { <pending> }
の配列が返ってきてしまう。
console.log(array.map(async i => (await echo(i)) * 2)) |
Promise.all で結果を待つ
上記に加え、全体を Promise.all で囲うことで正しい結果を得ることができた。map の各要素は並列で処理されるので、待機時間も全体で 1 秒掛かり問題ない。
Promise.all(array.map(async i => (await echo(i)) * 2)).then(console.log) |
async/await 使わない版もいける
Promise.all(array.map(i => echo(i).then(v => v * 2))).then(console.log) |
map の代わりに for…of を使う
結果自体は正しく取得できるが、シーケンシャルにループしてしまうので push が 1 回呼ばれる度に 1 秒掛かってしまう。
;(async () => { |
これに関しては Promise.all で map を使ったほうがいいだろう。
filter
filter では 20 の倍数だけの配列に変換するようにしてみよう。
素の状態ではやはり正しく結果は返ってこない。
console.log(array.filter(i => echo(i) % 20 === 0)) |
map と同様に Promise.all で括ってもダメだ。フィルタされない値が返ってきてしまう。
Promise.all(array.filter(async i => (await echo(i)) % 20 === 0)).then( |
map との合わせ技
StackOverflow に良い方法があった。前述の Promise.all を使った map を組み合わせてフィルタする方法だ。
Promise.all(array.map(async i => (await echo(i)) % 20 === 0)) |
この処理だと非同期部分を map に負わせることができるので正しい結果が得られる。この方法はよく考えられていてなるほどと思った。分解して処理を追ってみる。
まず非同期の map を行っているが、map の callback に filter の条件を入れている。そうすると配列のどのインデックスが条件に合致するかという結果が boolean の配列で返される。
Promise.all(array.map(async i => (await echo(i)) % 20 === 0)) |
次にフィルタを掛けるわけだが、注目すべきは i => bits.shift()
だ。filter の callback では array の要素を i で参照できるが、それは全く使わずに bits の結果だけで判定している。bits はどのインデックスが条件に合致するかという boolean 配列なので、それを shift()
で順番に取得し、判定しているのだ。
.then(bits => array.filter(i => bits.shift())) |
async/await で書き直してみる
……一緒だな、これ
;(async () => { |
reduce
要素の合計
最後に reduce、全要素の合計を算出してみる。
例によってそのまま実行
const sum = array.reduce((acc, cur) => { |
凄まじい結果が返ってきてしまった。もちろん NG だ。
async/await で回す
reduce の callback を async/await で装飾し、accumulator の初期値を Promise.resolve(0)とすることで、accumulator(累積値)を全て Promise で回すことができる。
array |
これで正しい値が取得できた。
要素を再配列化
reduce を使って map と同じことしてみる。
array |
原理は一緒、ただ accumulator のメソッドを呼び出したい場合は一度変数(acc
)を経由して Promise を resolve する。また、return する時に Promise で再パックする必要がある。ちょいめんどい。
7/19 追記
もっと簡単に書けた
array |
要素をオブジェクト化
同様にオブジェクト化してみる。キーをインデックス、値はそのまま値として、以下のようなものに変換したい。
{ |
配列にする場合とほとんど同じ、違いは引数で index を参照するくらいか
array |
問題なく取得できた。
7/19 追記 2
こっちももっと簡単に書けた
array |
まとめ
メソッド | やり方 |
---|---|
forEach | for...of |
map | Promise.all + async/await |
filter | map(Promise.all + async/await ) + filter |
reduce | async/await + Promise.resolve |
色々書いているうちに Promise に対する理解が少し深まった気がした。
余談
全部書いてから下記を見つけた。これでいいじゃん…
まあ、探せばあるよねぇ。。。