Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
5
Help us understand the problem. What is going on with this article?
@momotaro98

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

More than 3 years have passed since last update.

はじめに

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
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
momotaro98
サーバサイド寄りのWebエンジニアです。QiitaでLGTMをもらうことで生を実感します。
rarejob
明治神宮にあるオンライン英会話サービスを提供するベンチャー

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
5
Help us understand the problem. What is going on with this article?