JavaScript
More than 5 years have passed since last update.

async.jsなどにあるwatarfallとparallelを再発明してみた。

Callbacksクラス

まずはCallbacksクラスを作ります。
使い方は簡単で、まず、非同期関数はCallbacksを返すようにします。次に、処理完了時のコールバック関数をdonefailの引数に渡します。そして、非同期関数内で処理が完了したらdoneCallbackfailCallbackを実行するだけです。

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個の非同期処理を上から順番に実行して、最後にdonefailの引数に渡した関数を実行します。ちなみに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);
});