はじめに
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だったら
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を使います。
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
をオリジナルのソースコードを元に実装してみます
// 自作の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を使って行うと次のようになります。
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を実装してみる
この処理もオリジナルのソースコードを元に実装してみます。
// 自作の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等についても今後できれば確認したいです。