非同期プログラミングを支援する
ライブラリやイディオムには、
Deferred, Promises, Async/Await
などがありますが…
Task.js も、
非同期プログラミングを
強力に支援するライブラリの1つです
このようなコードを毎回書いていませんか?
var progress = 0, tasks = 3; // 非同期処理は合計で3つ
var cursor = new WaitCursor().show(); // アニメーションカーソルを表示
window.addEventListener("load", done); // ページの読み込み完了を待つ
(new Session()).login({ callback: done }); // サーバからのレスポンスを待つ
(new Asset()).download({ callback: done }); // アセットのダウンロードを待つ
function done() {
if (++progress >= tasks) { // 3つの非同期処理が終わったら
cursor.hide(); // カーソルを非表示に
}
}
Task.js を使うと、
元のコードの原型を維持したまま、
非同期処理をコンパクトに構造化できます
var task = new Task(3, function() { // 3つの非同期処理の完了を待つ
cursor.hide(); // 処理完了でカーソルを非表示に
});
var cursor = new WaitCursor().show(); // アニメーションカーソルを表示
window.addEventListener("load", task.passfn()); // ページの読み込み完了を待つ
(new Session()).login({ callback: task.passfn() }); // サーバからのレスポンスを待つ
(new Asset()).download({ callback: task.passfn() }); // アセットのダウンロードを待つ
Task.js は、このような
Needs と Wants を
満たすように設計しています
では
Task.js の特徴と機能を見て行きましょう
var task = new Task(2, callback); // 同期+非同期の2つのユーザタスクの完了を待つ
userTask(); // ユーザタスクを同期実行
setTimeout(userTask, 1000); // ユーザタスクを非同期で実行
function userTask() {
Math.random() >= 0.8 ? task.pass() : task.miss(); // 80% の確率で pass
}
function callback(error) {
console.log("finished");
}
必要な知識はこの3つだけ
シンプルです
次のページからは、
Task.js をさらに便利に使いこなす方法を
紹介していきます
バッファに貯める | task.push(), task.set() |
バッファから取り出す | task.buffer(), callback(, buffer) |
型を変換する | Task.flatten(), Task.arraynize(), Task.objectize() |
function callback(error, buffer) {
console.log(buffer[0]); // -> "value1"
console.log(buffer.key2); // -> "value2"
}
var task = new Task(1, callback);
task.push("value1"); // buffer に "value1" を追加する
task.set("key2", "value2"); // buffer に { "key2": "value2" } をセット(上書き)する
task.pass();
function callback(error, buffer) { // sharedBuffer: ["junction", "value1", "value2"]
console.log(buffer.length); // -> 3
}
var junction = new Task(2, callback).push("junction");
var task1 = new Task(1, junction).push("value1"); // junction.push("value1") と同じ結果に
var task2 = new Task(1, junction).push("value2"); // junction.push("value2") と同じ結果に
task1.pass();
task2.pass();
var source = [
[1,2],
[3,4],
[ [5,6] ]
];
// 3次元配列を2次元配列に展開
var complexArray = Task.flatten(source); // -> [1,2,3,4,[5,6]]
// 2次元配列さらに展開し、フラットな配列に
var flattenArray = Task.flatten(complexArray); // -> [1,2,3,4,5,6]
var source = [1,2,3];
source["key"] = "value"; // 配列にプロパティを追加
Task.arraynize(source); // -> [1, 2, 3], 追加した { key, value } は捨てられています
var source = [1,2,3];
source["key"] = "value"; // 配列にプロパティを追加
Object.keys(source); // -> ["0", "1", "2", "key"]
Task.objectize(source); // -> { 0: 1, 1: 2, 2: 3, key: "value" }
タスク一覧をダンプ | Task.dump() |
タスク一覧をクリア | Task.clear() |
var task1 = new Task(1, function(){});
var task2 = new Task(1, function(){});
var task3 = new Task(1, function(){});
Task.dump();
{
"anonymous@1": { junction: false, taskCount: 1, missableCount: 0, missedCount: 0, passedCount: 0, state: "" }
"anonymous@2": { junction: false, taskCount: 1, missableCount: 0, missedCount: 0, passedCount: 0, state: "" }
"anonymous@3": { junction: false, taskCount: 1, missableCount: 0, missedCount: 0, passedCount: 0, state: "" }
}
var task1 = new Task(1, callback, { name: "Foo" });
var task2 = new Task(1, callback, { name: "Foo" });
var task3 = new Task(1, callback, { name: "Bar" });
Task.dump("Foo");
{
"Foo@1": { junction: false, taskCount: 1, missableCount: 0,
missedCount: 0, passedCount: 0, state: "" },
"Foo@2": { junction: false, taskCount: 1, missableCount: 0,
missedCount: 0, passedCount: 0, state: "" }
}
Task.clear(); // この行以前の Task のスナップショットを削除
var task = new Task(1, function() {}, { name: "debug" });
Task.dump();
{
"debug@1": { junction: false, taskCount: 1, missableCount: 0,
passedCount: 0, missedCount: 0, state: "" }
}
タスクの強制終了 | task.exit() |
状態取得, 終了判定 | task.state(), task.isFinished() |
エラーメッセージ | task.message() |
function callback(error) { }
var task = new Task(100, callback).missable(100);
task.exit(); // 強制的に待機終了にする -> callback(new Error(...))
var task = new Task(1);
task.isFinished(); // -> false
task.state(); // -> ""
task.pass(); // 待機成功で終了
task.isFinished(); // -> true
task.state(); // -> "pass"
状態 | task.state() | task.isFinished() |
---|---|---|
待機中 | "" | false |
待機成功で終了 | "pass" | true |
待機失敗で終了 | "miss" | true |
待機失敗で終了 | "exit" | true |
var task = new Task(1, function(error) {
console.log(error.message); // -> "O_o"
});
try {
throw new Error("O_o"); // -> 例外発生
task.pass(); // ここには到達しない
} catch (err) { // err.message は "O_o"
task.message( err ).miss(); // task.message("O_o") を設定
}
pass と miss を呼び分ける | task.done() |
pass または miss を実行する 関数を取得する |
task.passfn(), task.missfn() |
失敗を許容する | task.missable() |
もっと待機する | task.extend() |
var error = Math.random() >= 0.5 ? new Error("message")
: null;
// このようなありがちなエラーを判定するコードは
if (error) { // Error Object
task.message( error ).miss();
} else {
task.pass();
}
// task.done を使うと 一行で書けます
task.done( error );
var task = new Task(1);
// このようなコードは
setTimeout(function() { task.pass(); }, 1000);
// passfn を使ってこのようにも書けます
setTimeout(task.passfn(), 1000);
function callback(error) { console.log(error.message); }
var task = new Task(1, callback, { name: "MissableTask" });
task.missable(2); // 2回までの失敗を許容する(3回失敗したら終了する)
task.miss(); // ユーザタスク失敗(1回目の失敗なので継続する)
task.miss(); // ユーザタスク失敗(2回目の失敗なので継続する)
task.miss(); // ユーザタスク失敗(3回目の失敗なので待機失敗で終了する) -> callback(Error)
// 例: CDN1 からのダウンロードが失敗した場合に CDN2 でリカバリー
var urls = ["http://cdn1.example.com/image.png",
"http://cdn2.example.com/image.png"];
download( urls, new Task(urls.length, callback).missable(1) );
function download(urls, task) {
var xhr = new XMLHttpRequest();
xhr.onload = function() { task.pass(); };
xhr.onerror = function() {
// CDN1 miss ▶ 失敗が許容されている?(タスク継続中?) ▶ CDN2 でリカバリー
if ( !task.miss().isFinished() ) {
download(urls, task);
}
};
xhr.open("GET", urls.shift(), true);
xhr.send()
}
function callback(error) { console.log(error.message); }
function callback(error) { }
var taskCount = 1;
var task = new Task(taskCount, callback);
task.extend(1); // taskCount += 1;
task.pass(); // ユーザタスク成功(taskCount は2なので待機する)
task.pass(); // ユーザタスク成功(taskCount は2なので待機成功で終了する)
// -> callback(null)
Task を連結する | Junction |
並列/直列動作 | Task.run() |
function callback(error) {
console.log("finished");
}
var junction = new Task(2, callback);
var task1 = new Task(1, junction);
var task2 = new Task(1, junction);
task1.pass(); // →junction にも状態変化が通知される
task2.pass(); // →junction にも状態変化が通知される
// →junction の待機も終了する
function callback(error) {
console.log("finished");
}
var junction = new Task(2, callback);
var task1 = new Task(1, junction);
var task2 = new Task(1, junction);
task1.pass(); // →junction にも状態変化が通知される
task2.pass(); // →junction にも状態変化が通知される
// →junction の待機も終了する
function callback(error) {
console.log("finished");
}
lv1_junction = new Task(1, callback);
lv2_junction = new Task(1, lv1_junction);
lv3_junction = new Task(2, lv2_junction);
lv4_task1 = new Task(1, lv3_junction);
lv4_task2 = new Task(1, lv3_junction);
lv4_task1.pass();
lv4_task2.pass();
>
と +
でつなぐ事で、function callback(error, buffer) {
}
Task.run("task_a > task_b + task_c > task_d", {
task_a: function(task) { task.pass(); },
task_b: function(task) { task.pass(); },
task_c: function(task) { task.pass(); },
task_d: function(task) { task.pass(); }
}, callback);
var taskMap = {
a: function(task) { task.pass(); },
b: function(task) { task.pass(); },
c: function(task) { task.pass(); },
d: function(task) { task.pass(); },
};
var junction = new Task(2, callback); // (a > b) + (c + d) が終わったら callback
Task.run("a > b", taskMap, junction); // a を実行後に b を実行
Task.run("c + d", taskMap, junction); // c と d を並列実行
+
でつなぐと、
Task.run("task_a + task_b", {
task_a: function(task) { task.pass(); },
task_b: function(task) { task.pass(); },
}, callback);
>
でつなぐと、Task.run("task_a > task_b", {
task_a: function(task) { task.pass(); },
task_b: function(task) { task.pass(); },
}, callback);
Task.run("task_a > 1000 > task_b", {
task_a: function(task) { task.pass(); },
task_b: function(task) { task.pass(); },
}, callback);
function task_a(task) { task.pass(); }
function task_b(task) { task.pass(); }
function task_c(task) { task.pass(); }
// このように順番に実行するだけのユーザタスクは...
Task.run("task_a > task_b > task_c", {
task_a: task_a,
task_b: task_b,
task_c: task_c,
}, callback);
// シンプルに書くことができます
Task.run("", [task_a, task_b, task_c], callback);
a > b + c + 1000 > d
は、ユーザタスク a 〜 d を以下の順番で実行します
Task.run("a > b + c + 1000 > d", {
a: function(task) { task.pass(); },
b: function(task) { task.pass(); },
c: function(task) { task.pass(); },
d: function(task) { task.pass(); }
}, callback);
var arg = { a: 1, b: 2, c : 3, d: 4 }; // ユーザタスクに渡す値
Task.run("task_a > task_b + task_c > task_d", {
task_a: function(task, arg) { console.log(arg.a); task.pass(); },
/// /////
task_b: function(task, arg) { console.log(arg.b); task.pass(); },
task_c: function(task, arg) { console.log(arg.c); task.pass(); },
task_d: function(task, arg) { console.log(arg.d); task.pass(); },
}, function(error, buffer) {
if (error) {
console.log("ng");
} else {
console.log("ok");
}
}, { arg: arg });
////////////////////////
Task.run("task_a > task_b", {
task_a: function(task) { task.miss(); },
task_b: function(task) { task.pass(); }, // task_b は実行されません
}, callback);
Task.run("task_c + task_d + task_e", {
task_c: function(task) {
setTimeout(function() { task.miss() }, 1000); // 1000ms 後に失敗
},
task_d: function(task) { task.pass(); }, // task_c が中断しても task_d は中断しません
task_e: function(task) { task.pass(); }, // task_c が中断しても task_e は中断しません
}, callback);
Task.run("task_a + task_b + task_c", { // task_a, task_b, task_c が存在しない ▶ エラー
bad_argument: function(/* task */) {} // 引数を受け取らないユーザタスク ▶ エラー
}, function() {});
> TypeError: Task.run(taskRoute, taskMap)
非同期ループ | Task.loop() |
var source = { a: 1, b: 2 }; // source には Object または Array を指定できます
Task.loop(source, tick, callback);
function tick(task, key, source) { // key は "a", "b" になります
console.log(key, source[key]); // -> a 1
// -> b 2
task.pass();
}
function callback(error, buffer) {
console.log("finished");
}
非同期の4つのユーザタスク A, B, C, D を、
以下の条件で実装する例です
function waitForAsyncProcesses(finishedCallback) {
var remainTaskGroupCount1 = [A, B].length; // 2
var remainTaskGroupCount2 = [C, D].length; // 2
var remainJunctionTaskCount = 2;
function A() { setTimeout(doneTaskGroup1, 10); }
function B() { setTimeout(doneTaskGroup1, 100); }
function C() { setTimeout(doneTaskGroup2, 20); }
function D() { setTimeout(doneTaskGroup2, 200); }
function doneTaskGroup1() {
if (--remainTaskGroupCount1 <= 0) { junction(); }
}
function doneTaskGroup2() {
if (--remainTaskGroupCount2 <= 0) { junction(); }
}
function junction() {
if (--remainJunctionTaskCount <= 0) { finishedCallback(); }
}
A(); B(); C(); D();
}
waitForAsyncProcesses(function(error) { console.log("finished"); });
function waitForAsyncProcesses(finishedCallback) {
var promises1 = [A(), B()]; // 2
var promises2 = [C(), D()]; // 2
function A() {
var dfd = jQuery.Deferred();
setTimeout(function() { dfd.resolve(); }, 10);
return dfd.promise();
}
function B() {
var dfd = jQuery.Deferred();
setTimeout(function() { dfd.resolve(); }, 100);
return dfd.promise();
}
function C() {
var dfd = jQuery.Deferred();
setTimeout(function() { dfd.resolve(); }, 20);
return dfd.promise();
}
function D() {
var dfd = jQuery.Deferred();
setTimeout(function() { dfd.resolve(); }, 200);
return dfd.promise();
}
jQuery.when(
jQuery.when.apply(null, promises1), // task group1
jQuery.when.apply(null, promises2) // task group2
).done(function() {
finishedCallback()
});
}
waitForAsyncProcesses(function(error) { console.log("finished"); });
function waitForAsyncProcesses(finishedCallback) {
function A() {
return new Promise(function(resolve, reject) { setTimeout(resolve, 10); });
}
function B() {
return new Promise(function(resolve, reject) { setTimeout(resolve, 100); });
}
function C() {
return new Promise(function(resolve, reject) { setTimeout(resolve, 20); });
}
function D() {
return new Promise(function(resolve, reject) { setTimeout(resolve, 200); });
}
Promise.all([
Promise.all([A(), B()]),
Promise.all([C(), D()])
]).then(function() {
finishedCallback(null);
}).catch(function(error) {
finishedCallback(error);
});
}
waitForAsyncProcesses(function(error) { console.log("finished"); })
function waitForAsyncProcesses(finishedCallback) {
var taskMap = {
A: function(task) { setTimeout(task.passfn(), 10); },
B: function(task) { setTimeout(task.passfn(), 100); },
C: function(task) { setTimeout(task.passfn(), 20); },
D: function(task) { setTimeout(task.passfn(), 200); },
};
var junction = new Task(2, finishedCallback);
var taskGroup1 = new Task(2, junction);
var taskGroup2 = new Task(2, junction);
taskMap.A(taskGroup1);
taskMap.B(taskGroup1);
taskMap.C(taskGroup2);
taskMap.D(taskGroup2);
}
waitForAsyncProcesses(function(error) { console.log("finished"); });
function waitForAsyncProcesses(finishedCallback) {
var taskMap = {
A: function(task) { setTimeout(task.passfn(), 10); },
B: function(task) { setTimeout(task.passfn(), 100); },
C: function(task) { setTimeout(task.passfn(), 20); },
D: function(task) { setTimeout(task.passfn(), 200); },
};
var junction = new Task(2, finishedCallback);
Task.run("A + B", taskMap, junction);
Task.run("C + D", taskMap, junction);
}
waitForAsyncProcesses(function(error) { console.log("finished"); });
github
https://github.com/uupaa/Task.js
npm
$ npm install uupaa.task.js
Node.js
var Task = require("uupaa.task.js");
var task = new Task(1, ...);
Browser
<script src="uupaa.task.js"></script>
<script>
var task = new Task(1, ...);
</script>
WebWorkers
importScripts("uupaa.task.js");
var task = new Task(1, ...);
DevTools
new Task(1, function() { console.log("Hello Task.js"); }).pass();
(ε・◇・)з o O ( Task.js オススメです