LoginSignup
5
5

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-02-05

はじめに

asyncという主にNode.jsで使用されている、JavaScriptの非同期処理を扱うライブラリがあります。

asyncには非同期を含む処理を順番に実行するseriesというメソッドがあり、Qiitaの記事では、こちらの記事で取り上げられています。

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

※1: というのも私が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だったら

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!");
出力
Finished!
template.html を処理しました
script.js を処理しました
style.css を処理しました

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

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

async.seriesを使う

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

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!");
});
出力
template.html を処理しました
style.css を処理しました
script.js を処理しました
Finished!

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

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

async.seriesを実装してみる

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

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!");
});

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

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

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

エラー処理もしてみる

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

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

async.seriesを使う

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

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!");
  }
});
出力
template.html を処理しました
High Cost Error! File name: style.css Cost: 800

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

async.seriesを実装してみる

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

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等についても今後できれば確認したいです。

参考

参考記事

参考コード

5
5
0

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
5
5