4
4

More than 5 years have passed since last update.

CoffeeScriptで配列に対して非同期関数を含む処理を1個ずつ実行する

Last updated at Posted at 2013-02-23

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

サンプル1 1秒に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変数だけですが、何個でも渡せます。

サンプル2 オブジェクト持ちまわし
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してループ処理を中断。

サンプル3 エラー処理(例外を投げる)
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()で作成したエラーオブジェクトをセットして呼び出してください。

サンプル4 エラー処理(next(エラーオブジェクト))
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を再現すると以下のようになります。

サンプル2 async.eachSeries()版
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

結果的には車輪の再発明ネタでしたけど、いい勉強になりました。

4
4
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4