async.jsなどにあるwatarfallとparallelを再発明してみた。
Callbacksクラス
まずはCallbacks
クラスを作ります。
使い方は簡単で、まず、非同期関数はCallbacks
を返すようにします。次に、処理完了時のコールバック関数をdone
、fail
の引数に渡します。そして、非同期関数内で処理が完了したらdoneCallback
、failCallback
を実行するだけです。
var Callbacks = (function() {
var noop = function(){};
function Callbacks() {
this.doneCallback = this.failCallback = noop;
}
Callbacks.prototype.done = function(callback) {
this.doneCallback = callback;
return this;
};
Callbacks.prototype.fail = function(callback) {
this.failCallback = callback;
return this;
};
return Callbacks;
})();
// msミリ秒後に何か実行
function wait(ms) {
var callbacks = new Callbacks;
setTimeout(function() {
callbacks.doneCallback();
}, ms);
return callbacks;
}
wait(1000).done(alert.bind(window, 'hello world'));
// ロードしたら何か実行
function load(url) {
var callbacks = new Callbacks;
var xhr = new XMLHttpRequest;
xhr.open('GET', url);
xhr.onloadend = function() {
var status = xhr.status;
if (status === 200 || status === 206) {
callbacks.doneCallback(xhr.responseText);
} else {
callbacks.failCallback('Load error: ' + status);
}
};
xhr.send();
return callbacks;
}
load('foo.txt')
.done(function(foo) {
console.log(foo)
})
.fail(function(e) {
cansole.log(e);
});
waterfall関数
waterfall関数は引数にCallbacks
を返す関数をn個受け取りCallbacks
を返します。
そのn個の非同期処理を上から順番に実行して、最後にdone
、fail
の引数に渡した関数を実行します。ちなみにthis
のコンテキストは共有されます。
function waterfall(funcs) {
var callbacks = new Callbacks, context = {};
funcs = Array.isArray(funcs) ? funcs : Array.prototype.slice.call(arguments);
function doneCallback() {
callbacks.doneCallback.apply(context, arguments);
}
function failCallback(e) {
callbacks.failCallback(e);
}
// 前から1個ずつ順番に実行していく
function _waterfall() {
funcs
.shift()
.apply(context, arguments)
.done(funcs.length > 0 ? _waterfall : doneCallback)
.fail(failCallback);
}
_waterfall();
return callbacks;
}
waterfall(function() {
this.hoge = 1;
return wait(1000);
}, function() {
console.log(this.hoge);
console.log('foo');
return wait(2000);
}, function() {
console.log('bar');
return load('foo.txt');
})
.done(function(foo) {
console.log(foo);
})
.fail(function(e) {
console.log(e);
});
parallel関数
parallel関数は引数にn個のCallbacks
を受け取りCallbacks
を返します。
n個の非同期処理が全て完了したときにその結果をdone
の引数に渡した関数から配列として受け取ることができます。失敗時はfail
の引数に渡した関数を即座に実行します。
function parallel(callbacksArr) {
callbacksArr = Array.isArray(callbacksArr) ? callbacksArr : Array.prototype.slice.call(arguments);
var callbacks = new Callbacks,
results = [],
count = callbacksArr.length,
isError = false;
function doneCallback(i, result) {
// エラーがすでに起きている場合は何もしない
if (isError) return;
count--;
results[i] = result;
// 全て終了したらdoneCallbackを実行
if (!count) callbacks.doneCallback(results);
}
function failCallback(e) {
isError = true;
callbacks.failCallback(e);
}
callbacksArr.forEach(function(callbacks, i) {
callbacks
.done(doneCallback.bind(null, i))
.fail(failCallback);
});
return callbacks;
}
parallel(
load('foo.txt'),
load('bar.txt'),
load('baz.txt')
)
.done(function(texts) {
texts.forEach(function(text) {
console.log(text);
});
})
.fail(function(e) {
console.log(e);
});
まとめ
waterfallもparallelも意外と簡単に実装できるってことがわかったと思います。
ちなみにwaterfall関数とparallel関数はどちらもCallbacks
を返すので、相互に組み合わせることができます。
waterfall(function() {
return parallel(load('foo.txt'), load('bar.txt'));
}, function(texts) {
this.texts = texts;
return parallel(load('hoge.js'), load('fuga.js'));
}, function(scripts) {
this.scripts = scripts;
return wait(1000);
})
.done(function() {
console.log(this.texts);
console.log(this.scripts);
})
.fail(function(e) {
console.log(e);
});