Node.jsで使うasync.seriesから非同期対応シーケンシャル処理の実装を理解する

# はじめに
[async](http://caolan.github.io/async/)という主にNode.jsで使用されている、JavaScriptの非同期処理を扱うライブラリがあります。

asyncには非同期を含む処理を順番に実行するseriesというメソッドがあり、Qiitaの記事では、[こちらの記事](http://qiita.com/takeharu/items/84ffbee23b8edcbb2e21#series)で取り上げられています。

今回、seriesのソースコードを読み(※1)、非同期に対応するシーケンシャル処理の実装方法を把握することができたので記事にしました。

※1: というのも私が[octotree](https://github.com/buunguyen/octotree)というChrome拡張機能のソースコードを読み進めていたところ、今回扱う処理方法に出会い、調べたところ、seriesの実装と同様のものとわかったという流れなのです。参考にしたコードのリンクは最後に示します。

### コードの実行環境
| | Version|
|:-----------|------------:|
| Node.js    |   6.8.1 |
| asnyc     |    2.1.4 |


ES6で書いてます


# メインの内容
## この記事で扱うシーケンシャル処理の例

`files`として以下のような配列を設定

```
const files = [
  {name: "template.html", cost: 100},
  {name: "style.css", cost: 800},
  {name: "script.js", cost: 400},
];
```

`template.html`, `style.css`, `script.js`の3ファイルを **順番に** 処理し、最後に`console.log("finished!")`を出力したいという想定です。

ファイルごとに`cost`があり、非同期処理である、`setTimeout`の第2引数に渡すことで擬似的に処理時間をばらつかせます。


## async.seriesじゃなくてforEachだったら

```javascript:syncloop.js
function makeTasks(files) {
  return files.map((file) => {
    return () => {
      setTimeout(() => {
        console.log(file.name + " を処理しました");
      }, file.cost);
    }
  })
}

const files = [
  {name: "template.html", cost: 100},
  {name: "style.css", cost: 800},
  {name: "script.js", cost: 400},
];

var tasks = makeTasks(files);
tasks.forEach((task) => {  // forEachを使う
  task();
});

console.log("Finished!");
```

```bash:出力
Finished!
template.html を処理しました
script.js を処理しました
style.css を処理しました
```

`tasks`は、`[() => {...}, () => {...}, () => {...}]`といった各ファイルに対応する関数が入った配列です。

これを`forEach`でループさせると、非同期処理であるので、Finished!->html->js->cssの順番で出力されてしまいます。

## async.seriesを使う

そこでasync.seriesを使います。


```javascript:asyncSeries.js
const async = require('async');

function makeTasks(files) {
  return files.map((file) => {
    return (callback) => {
      setTimeout(() => {
        console.log(file.name + " を処理しました");
        callback();  // seriesから受け取ったコールバックを発火させる必要がある
      }, file.cost);
    }
  })
}

const files = [
  {name: "template.html", cost: 100},
  {name: "style.css", cost: 800},
  {name: "script.js", cost: 400},
];

var tasks = makeTasks(files);
async.series(tasks, () => {  // async.seriesを使う
  console.log("Finished!");
});
```

```bash:出力
template.html を処理しました
style.css を処理しました
script.js を処理しました
Finished!
```

`async.series`を使うことで、今度は希望通りに実行できました。

`series`の第1引数にはタスクの入った配列、第2引数にはタスクがすべて終わったら実行させるコールバック関数を渡します。
そして`tasks`内の関数の引数としてコールバック関数を設定し、処理の最後に **必ず** 発火させる必要があります。

## async.seriesを実装してみる

次にこの`async.series`をオリジナルのソースコードを元に実装してみます


```javascript:mySeries.js
// 自作のseries
function mySeries(tasks, done) {
  (function next(index = 0) {
    if (index === tasks.length) done && done();
    else tasks[index](() => { next(++index); });  // 再帰
  })();
}

function makeTasks(files) {
  return files.map((file) => {
    return (callback) => {
      setTimeout(() => {
        console.log(file.name + " を処理しました");
        callback();
      }, file.cost);
    }
  })
}

const files = [
  {name: "template.html", cost: 100},
  {name: "style.css", cost: 800},
  {name: "script.js", cost: 400},
];

var tasks = makeTasks(files);
mySeries(tasks, () => {  // 自作のseriesを使う
  console.log("Finished!");
});

```

```bash:出力
template.html を処理しました
style.css を処理しました
script.js を処理しました
Finished!
```

自作のseriesでも同じように動作しました！
`mySeries`関数がseriesにあたります。

seriesの実装における注目ポイントはずばり **再帰** です。
`next`が再帰関数であり、`tasks`内の関数に`() => { next(++index); }`をコールバック関数として渡しています。受取側でこのコールバック関数を最後に実行させることで、順番に処理することを保証しています。
逆にコールバック関数を実行させないと1つ目の処理で終了してしまいます。


# エラー処理もしてみる

async.seriesでのエラー処理もついでに

ここでは、`cost`が500より大きいファイルを扱うとエラーになる設定にします。

## async.seriesを使う

エラー処理をseriesを使って行うと次のようになります。

```javascript:asyncSeried-handleError.js
const async = require('async');

function makeTasks(files) {
  return files.map((file) => {
    return (callback) => {
      setTimeout(() => {
        if (file.cost > 500) {
          // エラーをコールバックに渡す
          callback("High Cost Error! File name: " + file.name + " Cost: " + file.cost);
        } else {
          console.log(file.name + " を処理しました");
          callback();
        }
      }, file.cost);
    }
  })
}

const files = [
  {name: "template.html", cost: 100},
  {name: "style.css", cost: 800},
  {name: "script.js", cost: 400},
];

var tasks = makeTasks(files);
async.series(tasks, (err) => {  // 処理中に発生したエラーを受け取る
  if (err) {
    // Handle Error
    console.log(err);
  } else {
    console.log("Finished!");
  }
});
```

```bash:出力
template.html を処理しました
High Cost Error! File name: style.css Cost: 800
```

このように、`async.series`では、エラーが起こると途中で中断され、第2引数のコールバック関数からエラーが渡されます。

## async.seriesを実装してみる

この処理もオリジナルのソースコードを元に実装してみます。

```javascript:mySeries-handleError.js
// 自作のseries
function mySeries(tasks, done) {
  (function next(index = 0) {
    if (index === tasks.length) done && done();
    else {
      tasks[index]((err) => {  // エラーを受け取る
        if (err) {
          done(err);  // 第2引数のコールバックにエラーを渡す
          return;
        }
        next(++index);
      });
    }
  })();
}

function makeTasks(files) {
  return files.map((file) => {
    return (callback) => {
      setTimeout(() => {
        if (file.cost > 500) {
          callback("High Cost Error! File name: " + file.name + " Cost: " + file.cost);
        } else {
          console.log(file.name + " を処理しました");
          callback();
        }
      }, file.cost);
    }
  })
}

const files = [
  {name: "template.html", cost: 100},
  {name: "style.css", cost: 800},
  {name: "script.js", cost: 400},
];

var tasks = makeTasks(files);
mySeries(tasks, (err) => {  // 自作のseriesを使う
  if (err) {
    // Handle Error
    console.log(err);
  } else {
    console.log("Finished!");
  }
});
```

このようにseries側は受け取ったエラーを`done`関数に渡す操作を内部で行っています。

## 他にも

async.seriesでは上述のエラーと同様に、tasksの関数に生成した値を渡すことで、第2引数のコールバックからそれらが配列になった値が得られますが、その機能も同様に実装されています。


# おわりに 

async.seriesの実装方法を確認しました。

非同期対応のシーケンシャル処理実装のポイントは再帰であり、下記の参考コード等でも確認できました。重要な処理方法なようです。

asyncのその他のメソッドの、waterfall, parallel等についても今後できれば確認したいです。

# 参考
## 参考記事
* [async公式ドキュメント](http://caolan.github.io/async/docs.html#series)
* [http://qiita.com/takeharu/items/84ffbee23b8edcbb2e21](http://qiita.com/takeharu/items/84ffbee23b8edcbb2e21#series)

## 参考コード
* [https://github.com/caolan/async/blob/master/lib/internal/eachOfLimit.js](https://github.com/caolan/async/blob/master/lib/internal/eachOfLimit.js)
* [https://github.com/buunguyen/octotree/blob/master/src/config/chrome/background.js](https://github.com/buunguyen/octotree/blob/master/src/config/chrome/background.js)
* [https://github.com/dominictarr/map-stream/blob/master/index.js]()