JavaScriptでPromiseの配列を直列に実行する

js logo

今日は JavaScript の非同期処理に使われるPromiseを直列に実行する方法をご紹介したいと思います。

JavaScript のforEachmapなどのイテレータ関数は返す値の順番は保障しますが、コールバックの実行は順番を保証しません。

バッチなどの処理で、Promiseをどうしても配列の順番で直列に実行する必要がある場合、forEachなどでそのまま書くと順番がちぐはぐになってハマります。

対処

唯一コールバックを順番に実行するイテレータ関数のreduceを使い、Promisethen chain を作ることで、処理が直列で実行されます。

テスト

Node の File System で 0-9 の配列を元に 10 個のファイルを出力。fsは非同期で手っ取り早く実装できるので。Promiseを返すwriteファンクションをforEach()Promise.all() + map()Promise + reduce()でそれぞれ実行する。

write function

fs のappendFile(ファイルがあれば追記、なければ作成)をPromiseで単純にラップしたものです。

ちなみにこの処理ならappendFileSync使えば一発ですが、あくまでも非同期処理の例ということですので。。

実行コマンド

$ node -e "require('./promise_array_test.js').EXPORTED_FUNCTION()"

実行は Node CLI でスクリプト直書きで行います。(-e オプションはスクリプトの実行)

普通の forEach()

普通のforEachです。当然ですが、lambda はパラレルで実行されます。

実行結果

$ node -e "require('./promise_array_test.js').forEachTest()"
just forEach ===========
done
0 is done!
2 is done!
1 is done!
8 is done!
6 is done!
5 is done!
7 is done!
4 is done!
9 is done!
3 is done!

doneが先に呼ばれて、そのあとパラレルでランダムに実行されています。

Promise.all と map()

Promise.allPromiseの配列をとってパラレルに実行し、それぞれの結果を正しい順番の配列で返します。(5 行目でPromiseを返すwrite function を呼んで配列を作ってPromise.allに渡しています。)

実行結果

$ node -e "require('./promise_array_test.js').promiseAllTest()"
promise all ============
1 is done!
0 is done!
5 is done!
7 is done!
4 is done!
2 is done!
3 is done!
8 is done!
6 is done!
9 is done!
done

Promise.allthenチェーンがあるので、処理がパラレルで全て実行され、doneがプリントされます。

Promise と reduce()

reduce()は配列の中の要素を順番に取ります。

第一引数のacc(accumulator)にはその前の lambda の結果(返り値)が入り、第二引数curには配列の要素が入ります。JS のイテレータらしく第三引数にはcurindexが入ります。

reduce()の第二引数は初期値(initial value)として、最初にaccに入る値を渡すことができます。

reduce((acc, cur) \=> acc + cur, 10);

初期値を渡さずに[0,1,2,3,4,5].reduce()を実行する場合、初回はaccには 0 が入り、curには 1、indexは 1 になりますので、注意が必要です。

関数の説明はさておき、要は『accPromiseを入れることによって lambda のthenチェーンを作る』という実装になります。

初期値にPromise.resolve()を渡すことによって、acc.then()のように始めることができます。

(ちなみにPromise.resolve()new Promise((res, rej) => res())と同じことです。Promise.reject()も同様。)

実行結果

$ node -e "require('./promise_array_test.js').promiseReduceTest()"
promise reduce ===========
0 is done!
1 is done!
2 is done!
3 is done!
4 is done!
5 is done!
6 is done!
7 is done!
8 is done!
9 is done!
done

美しく順番に実行されています。

全体

参考

COPYRIGHT © 2023 Kohei Ando