[js非同期] Calback教Async派の信者だけどPromise教bluebird派に改宗しようか悩んでる

> この記事は以下の環境を元に記事にしています
> - node v8.9.1
> - node/async   2.6.0
> - node/blubird 3.5.1


私にとってjavascriptの非同期処理はcallback一択でした。

コールバック地獄を免れる為に[async ](http://caolan.github.io/async/docs.html#parallel)や[CoffeeScript](http://coffeescript.org/)と仲良くなりました。



- asyncはコールバック処理での並列処理やループ等を素敵に行えるライブラリです。
- いわゆる[async await]とは別モノです
- coffee-scriptはコールバック地獄の括弧多すぎ問題から脱却できます



しかし昨今の主流はPromiseになっているようで、完全に取り残されてしまいました。

そこで今更ながらPromiseの学習を始めます。(async/awaitはまたの機会に)



今回は私が今までコールバック(+async)で実装してきた処理を意識して、promiseとの違いを書いていこうと思います。



## async

asyncはnode.jsの非同期処理を簡便に扱えるライブラリです。特にループ処理をする時には欠かせません。

[async \- Documentation](http://caolan.github.io/async/docs.html)

```
# install
npm install async
# require
async = require('async')
```

> `async`は`async/await`の登場もありそのままの変数名を使い続けて良いのかわかりませんが、上記のrequireで正常に動作します。しかしいつどこで不具合が出ないとも限りませんので`_async`等の変数名を使った方が良さそうです。





## bluebird

bluebirdはV8のPromiseを拡張したようなライブラリです。

map,reduce,filter等の**Array.prototype**に定義されているようなメソッド郡が使用できます。

[API Reference \| bluebird](http://bluebirdjs.com/docs/api-reference.html)

```
# install
npm install bluebird
# require
Promise = require('bluebird')
```



## callbackとPromiseの非同期関数例

今回の検証の為に以下のメソッドを用意しました。

(以下全てcoffee-scriptで書いています)



### callback

定義

```coffeescript
callbackFn = (startTime,delay,cb)->
  obj =
    delay: delay
    start: Date.now() - startTime
  setTimeout ->
    obj.end = Date.now() - startTime
    cb(null,obj)
  ,delay

```

使用例

```coffeescript

callbackFn Date.now(),100,(err,res)->
  console.log err if err
  console.log res

# { delay: 100, start: 0, end: 106 }
```



### Promise

定義

```coffeescript
promiseFn = (startTime,delay)->
  new Promise (res,err)->
    obj =
      delay: delay
      start: Date.now() - startTime
    setTimeout ->
      obj.end = Date.now() - startTime
      res(obj)
    ,delay
```

使用例

```coffeescript
asyncFn(Date.now(),100)
.then(console.log)
.catch(console.log)

# { delay: 100, start: 0, end: 104 }
```



上記はcallbackとpromiseでそれぞれ書いた同じメソッドです。

しかしcallback形式で書いた関数があればpromise形式に変換する方法があります。

```coffeescript
Promise = require 'bluebird'
promiseFn = Promise.promisify(callbackFn)
```

このように`Promise.promisify`を使うだけで変換できます。(bluebirdの機能です)

ただし条件があります。変換に用いる関数は

1. 最後の引数はコールバック関数を定義すること
2. コールバック関数の第１引数はerrorであること
3. コールバック関数の第２引数はresult(返り値)であること

これさえクリアできていれば変換できます。



## 1: Parallel(並列処理)

複数の非同期関数を並列処理し、配列に格納して返します。

(以下、mochaで動かしたテストもどきコードを掲載しています)



### async.parallel(tasks, callback)

```coffeescript
it "async.parallel",(done) ->
  startTime = Date.now()
  async.parallel [
    (next)-> callbackFn(startTime,100,next)
    (next)-> callbackFn(startTime,500,next)
    (next)-> callbackFn(startTime,300,next)
  ],(err,results)->
    return done err if err
    console.log results
    done()

# results:
[ { delay: 100, start: 0, end: 106 },
  { delay: 500, start: 0, end: 506 },
  { delay: 300, start: 0, end: 303 } ]
```

resultsのstartが全て0になっており、同時に処理していることがわかります。



> ちなみに`callbackFn`を呼び出している部分は、`async.apply`を使えば以下の様に書き換えられます。

```coffeescript
(next)-> callbackFn(startTime,100,next)
↓
async.apply(callbackFn,startTime,100)
```

> coffee-scriptではあまり変わらないように見えますがJSで書くと以下の様な違いになります。

```js
(function(next) {
  return callbackFn(startTime, 100, next);
});
↓
async.apply(callbackFn, startTime, 100);
```



### Promise.all(tasks)

```coffeescript
it "Promise.all", ->
  startTime = Date.now()
  Promise.all [
    promiseFn(startTime,100)
    promiseFn(startTime,500)
    promiseFn(startTime,300)
  ]
  .then(console.log)
  .catch(console.log)
  
# result:
[ { delay: 100, start: 0, end: 105 },
  { delay: 500, start: 0, end: 502 },
  { delay: 300, start: 0, end: 304 } ]
```

`then`と`catch`に渡す部分がスッキリしています。



> 上記はmochaのテスト形式ですがasyncの方は`it "async.parallel",(done) ->`から始まるのに対してPromiseの方は`it "Promise.all", ->`となっています。つまり`(done)`がありません。
>
> mochaで非同期関数をテストする時には`(done)`というコールバック関数を引数に与える必要があります。
>
> これは`done()`が呼ばれるまでテスト継続だよ、という意味です。
>
> しかしPromiseの場合は`Promiseオブジェクト`を返すと自動的に非同期処理する部分を汲み取ってくれるために`done`が不要になっているのです。
>
> [MochaがPromisesのテストをサポートしました \| Web Scratch](http://efcl.info/2014/0314/res3708/)



## 2: map

1のParallelでは配列に非同期処理メソッドを直接書いていました。

これはこれで使いみちもあるのですが、引数だけを変えて同じ関数を呼び出す時は`map`を使った方が便利です。



### async.map(coll, iteratee, callback)

第１引数はCollection(配列)

第２引数は反復処理の関数

第３引数は反復処理の終了後の関数

```coffeescript
it "async.map",(done) ->
  startTime = Date.now()
  async.map [100,500,300],((delay,next)->
    callbackFn(startTime,delay,next))
  ,(err,results)->
    return done err if err
    console.log results
    done()
    
# results:
[ { delay: 100, start: 0, end: 106 },
  { delay: 500, start: 0, end: 506 },
  { delay: 300, start: 0, end: 306 } ]
```



### Promise.map(coll,iteratee,opt)

第１引数はCollection(配列)

第２引数は反復処理の関数

```coffeescript
it "map", ->
  startTime = Date.now()
  Promise.map [100,500,300],(ms)->
    promiseFn(startTime,ms)
  .then(console.log)
  .catch(console.log)
  
# results:
[ { delay: 100, start: 1, end: 102 },
  { delay: 500, start: 1, end: 503 },
  { delay: 300, start: 1, end: 305 } ]
```





## 3: mapLimit

APIを100回叩くような場合に`map`を使うと、一気に100のリクエストを送ることになります。これはサーバーに負担をかけることになりますしDOS攻撃だと受け取られかねません。

`mapLimit`は同時に行う処理の上限を指定することができます。



### async.mapLimit(coll,limit, iteratee, callback)

`async.map`との違いは第２引数が`limit`になっている所です。同時処理のlimit数を設定します。

```coffeescript
it "async.mapLimit",(done) ->
  startTime = Date.now()
  async.mapLimit [100,500,300],2,((delay,next)->
    callbackFn(startTime,delay,next))
  ,(err,results)->
    return done err if err
    console.log results
    done()

# results:
[ { delay: 100, start: 1, end: 104 },
  { delay: 500, start: 1, end: 505 },
  { delay: 300, start: 104, end: 405 } ]
```

まず２つの処理が同時に行われ、最初の処理が104ms後に終了、同時に３つ目の処理が始まっています。



### Promise.map(coll,iteratee,opt)

使用する関数は同じ`map`ですが、第三引数を`{concurrency: 2}`とすることでlimit数を2に制限しています。

```coffeescript
it "map limit", ->
  startTime = Date.now()
  Promise.map [100,500,300],(ms)->
    promiseFn(startTime,ms)
  ,{concurrency: 2}
  .then(console.log)
  .catch(console.log)

# results:
[ { delay: 100, start: 1, end: 101 },
  { delay: 500, start: 1, end: 502 },
  { delay: 300, start: 101, end: 406 } ]
```



## 4: mapSeries

`mapSeries`は同時に行う処理数を１つにします。完全な直列処理です。

mapLimit 1と同義です。



### async.mapSeries(coll, iteratee, callback)

`async.map`と使い方は全く同じです。ただ直列で処理されます。

```coffeescript
it "async.mapSeries",(done) ->
  startTime = Date.now()
  async.mapSeries [100,500,300],((delay,next)->
    callbackFn(startTime,delay,next))
  ,(err,results)->
    return done err if err
    console.log results
    done()

# results:
[ { delay: 100, start: 1, end: 104 },
  { delay: 500, start: 104, end: 609 },
  { delay: 300, start: 609, end: 914 } ]
```

endと次行のstartが同じ値になっており、直列で処理していることがわかります。





### Promise.mapSeries(coll,iteratee)

`Promise.map`と使い方は全く同じです。ただ直列で処理されます。

```coffeescript
it "map series", ->
  startTime = Date.now()
  Promise.mapSeries [100,500,300],(ms)->
    promiseFn(startTime,ms)
  .then(console.log)
  .catch(console.log)

# results:
[ { delay: 100, start: 1, end: 103 },
  { delay: 500, start: 103, end: 605 },
  { delay: 300, start: 605, end: 908 } ]
```



## 5: series

直列処理です。map系と違うのは引数を変えて同じ関数を叩くのではなく、全く別の処理を行うという所です。

`Promise.all`の直列処理バージョンだと思ってください。



### async.series(tasks, callback)

```coffeescript
it "async.series",(done) ->
  startTime = Date.now()
  async.series [
    async.apply(callbackFn,startTime,100)
    async.apply(callbackFn,startTime,500)
    async.apply(callbackFn,startTime,300)
  ],(err,results)->
    return done err if err
    console.log results
    done()
    
# results:
[ { delay: 100, start: 0, end: 104 },
  { delay: 500, start: 104, end: 608 },
  { delay: 300, start: 608, end: 910 } ]
```

上記では`callbackFn`を3回繰り返していますのでmapSeriesと同じ結果になっていますが、全く別の関数を当てはめることが可能だという点に着目してください。



### Promise

Promise(bluebird)には`series`に当てはまるメソッド(allの直列バージョン)は無いようです。

(あったら教えてください)

そもそもPromiseのthenは直列に処理できる方法なので、以下の様になら書けます。

```coffeescript
it "series", ->
  startTime = Date.now()
  arr = []
  Promise.resolve()
  .then -> promiseFn(startTime,100)
  .then (val)-> arr.push(val)
  .then -> promiseFn(startTime,500)
  .then (val)-> arr.push(val)
  .then -> promiseFn(startTime,300)
  .then (val)-> arr.push(val)
  .then(->console.log(arr))
  .catch(console.log)
```

一つの処理に2回thenしてるのが気持ち悪いです(-_-;)



## 6: waterFall

waterFallは取得した値を次のメソッドに渡します。

説明しにくいのでコードを見てください。

### async.waterfall(tasks, callback)

```coffeescript
it "async.waterfall",(done) ->
  startTime = Date.now()
  items = []
  async.waterfall [
    (next)->
      callbackFn startTime,100,(err,item)->
        items.push(item)
        next(null,item.end)
    (endValue,next)->
      callbackFn startTime,endValue,(err,item)->
        items.push(item)
        next(null,item.start,item.end)
    (startValue,endValue,next)->
      value = startValue + endValue
      callbackFn startTime,value,(err,item)->
        items.push(item)
        next(null)
  ],(err,results)->
    return done err if err
    console.log items
    done()

# items:
[ { delay: 100, start: 0, end: 102 },
  { delay: 102, start: 102, end: 207 },
  { delay: 309, start: 207, end: 517 } ]
```



`async.waterfall`の第１引数はタスク配列です。

最初のタスクでendの102を次に渡しています。

次のタスクでは102を受け取って処理、次にstartの102とendの207を渡しています。

最後のタスクでは受け取った102と207を足した309で処理をしています。



このような処理は使いたいケースが結構多いです。

twitterで例えるとAPIを叩いて人気のユーザーを抽出、そのユーザーの別のツイートを分析、みたいな事をするのには必須のメソッドです。



### Promise

Promise(bluebird)には`waterfall`に当てはまるメソッドは無いようです。

あえて書くなら以下の様な感じでしょうか。

```coffeescript
it "water fall", ->
  startTime = Date.now()
  items = []
  Promise.resolve()
  .then -> promiseFn(startTime,100)
  .then (item)->
    items.push(item)
    return item.end
  .then (end)-> promiseFn(startTime,end)
  .then (item)->
    items.push(item)
    return item
  .then (item)-> promiseFn(startTime,item.start+item.end)
  .then (item)->
    items.push(item)
  .then(->console.log(items))
  .catch(console.log)

# items:
[ { delay: 100, start: 1, end: 104 },
  { delay: 104, start: 104, end: 210 },
  { delay: 314, start: 211, end: 528 } ]
```

なかなか気持ち悪いコードになりました。



### promise-waterfall

npmに`promise-waterfall`というモジュールがありました。

[promise\-waterfall](https://www.npmjs.com/package/promise-waterfall)

これを使うとPromiseでwaterfallできるみたいなので使ってみました。



```coffeescript
it "promise-waterfall", ->
  waterfall = require("promise-waterfall")
  startTime = Date.now()
  items = []
  waterfall [
    ()->
      promiseFn(startTime,100)
      .then (item)->
        items.push(item)
        return item.end
    (end)->
      promiseFn(startTime,end)
      .then (item)->
        items.push(item)
        return item
    (item)->
      promiseFn(startTime,item.start+item.end)
      .then (item)->
        items.push(item)
  ]
  .then(->console.log(items))
  .catch(console.log)
  
# items:
[ { delay: 100, start: 0, end: 104 },
  { delay: 104, start: 105, end: 210 },
  { delay: 315, start: 211, end: 527 } ]
```

読みやすくなりましたが別モジュールを入れるまでのことなんでしょうか・・・





## まとめ

map系はPromiseの方がスッキリ書けそうです。

しかしwaterfallは実際によく使うのでPromise教に改宗するのは躊躇してしまいます。

(オススメの書き方があったら教えてください)



ですがPromise(bluebird)には気になるメソッドがたくさんありますのでもうちょっと調べてみようと思います。