CoffeeScriptというかNode.jsでforやforEachを使ってループ処理を行う場合、その処理の中に非同期関数が混じっていると1個ずつ順番に実行させるように制御するのはちょっと面倒です(よね?)。npmを漁ってみましたが自分の希望に完全にマッチするものは見つからなかったので自作しました。
※ Array::prototypeを拡張(汚染)しています。
関数名はとりあえずseq()としていますが、適当に変更してください(2箇所)。
coffeeでコンパイルすればJavaScriptでも使えます。
Array::seq ?= () ->
[ func, idx, cb, args... ] = arguments
if idx instanceof Function
cb = idx
idx = 0
return cb.apply @, [ undefined ].concat(args) if idx >= @.length
next = () =>
return cb arguments[0] if arguments[0] instanceof Error
Array::seq.apply @, [ func, idx + 1, cb ].concat(a for a in arguments)
try
func.apply @, [ @[idx], idx, next ].concat(args)
catch e
return cb e
Array.prototype.seq(item, idx, next[, ...])
-
item
: 配列内の処理対象の値 -
idx
: 配列内のインデックス -
next
: これを呼び出すと配列内の次のインデックスの値の処理に入る - 以降の引数にはループ内で持ちまわしたいオブジェクトなどを指定
サンプル1
data = [ 'aaa', 'bbb', 'ccc', 'ddd' ]
data.seq (item, idx, next) ->
setTimeout () =>
console.log item, idx
next() # 次の値へ
, 1000
, (err) -> # 全ループ処理が終了したときに実行されるコールバック
console.log err
aaa 0 # 1秒後
bbb 1 # 1秒後
ccc 2 # 1秒後
ddd 3 # 1秒後
undefined # エラーなし
サンプル2
ループ内でオブジェクト(list)を持ちまわして最後のコールバックに渡す。
※例ではlist変数だけですが、何個でも渡せます。
data = [ 'aaa', 'bbb', 'ccc', 'ddd' ]
data.seq (item, idx, next, list = {}) -> # 引数にlist追加(初回処理時に新規作成される)
setTimeout () =>
console.log item, idx
list[item] = idx # listに値をセット
next(list) # next()の引数にlistを渡して次の処理に持ちまわす
, 1000
, (err, list) -> # 最後のコールバックの引数にlistを追加
console.log err ? list
aaa 0 # 1秒後
bbb 1 # 1秒後
ccc 2 # 1秒後
ddd 3 # 1秒後
{ aaa: 0, bbb: 1, ccc: 2, ddd: 3 } # ループ内でセットした情報を最後に表示
サンプル3
処理内でthrowしてループ処理を中断。
data = [ 'aaa', 'bbb', 'ccc', 'ddd' ]
data.seq (item, idx, next) ->
throw new Error 'error!!!' if idx is 2 # 3個目の値の処理でエラー発生
setTimeout () =>
console.log item, idx
next()
, 1000
, (err) ->
console.log err # errにthrowしたエラー内容が入る
aaa 0 # 1秒後
bbb 1 # 1秒後
[Error: error!!!] # 3個目の処理(idx = 2)の時に例外発生
サンプル4
サンプル3は非同期関数外でのthrowのなので拾えますが、非同期関数内でthrowした場合は捕捉できません。非同期関数内でエラーを発生させてループ処理を中断する場合はnextの第一引数にnew Error()で作成したエラーオブジェクトをセットして呼び出してください。
data = [ 'aaa', 'bbb', 'ccc', 'ddd' ]
data.seq (item, idx, next) ->
setTimeout () =>
return next(new Error 'error!!!') if idx is 2 # 3個目の値の処理でエラー発生
console.log item, idx
next()
, 1000
, (err) ->
console.log err # errにthrowしたエラー内容が入る
aaa 0 # 1秒後
bbb 1 # 1秒後
[Error: error!!!] # 3個目の処理(idx = 2)の時に例外発生
注意点
ループ内の処理でnext()を呼び忘れるとそこでループが切れてコールバックも呼ばれない状態になってしまいます。
追記(2013-03-02):async.jsで同じような機能があるらしい
コメントでasync.jsについてご指摘があったので追記します。
今回の処理を実現するモジュールを探していた時にasync.jsは見つけたのですが、このあたりで内容を確認してみたところ、waterfall、series、parallelの紹介だけだったので、「async.jsは事前に任意の個数の関数を登録して順番に処理結果を引き渡しながら実行させるタイプのモジュールであって、配列内の値を同じ関数で順番に処理して回すような機能はないんだな」と勝手に勘違いしておりました。もし、調査時にasync.js本家の情報をちゃんと見ていたらこれを採用していたと思います。
せっかくなので、async.eachSeries()と今回作成したArray.prototype.seq()を比較してみました。
async.eachSeries()で今回のサンプル2を再現すると以下のようになります。
async = require 'async'
list = {}
idx = 0
async.eachSeries [ 'aaa', 'bbb', 'ccc', 'ddd' ], (item, next) ->
setTimeout () ->
console.log item, idx
list[item] = idx++
next()
, 1000
, (err) ->
console.log err ? list
aaa 0 # 1秒後
bbb 1 # 1秒後
ccc 2 # 1秒後
ddd 3 # 1秒後
{ aaa: 0, bbb: 1, ccc: 2, ddd: 3 } # ループ内でセットした情報を最後に表示
ほぼ同じ組み方でArray.prototype.seq()と同様の処理を行うことができます。
両者の違い
async.eachSeries()は、
- 配列の値を1つずつ処理する関数内で、現在処理している値の配列内でのインデックスが分からない。
- 配列の値を1つずつ処理する関数内で、別途処理したオブジェクトなどを次の処理へ持ちまわすことができない。
ですが、上記のサンプルのようにlistとidxを外で定義しておけば対処できます。
対してArray.prototype.seq()は、
- Array.prototypeを汚す。
という点が大きいかな、と思います。
結論
ほとんど使い方は変わらないのでこういった処理を求めている方は、上記で挙げたメリット・デメリットを比較した上で利用すれば良いと思います。
まあ、async.jsの方がgithubで管理されているだけあり、今後も改善や機能拡張もあるだろうし、何よりもeachSeries()の他にも機能が満載なので、Array.prototype.seq()を作っておきながら言うのも何ですが、async.js方がオススメですw
結果的には車輪の再発明ネタでしたけど、いい勉強になりました。