Google Closure LibraryのDeferredクラスでJSDeferredのloop相当のことをできるようにする

Google Closure LibraryではMochikitのDeferredがサードパーティライブラリとして提供されています(waitとか削られている機能もあるようですが)。
でもMochikitのDeferredではループ処理を遅延処理化する機能は提供されていないので、できるようにしてみました。
JSDeferredでいうところのloop関数に相当するやつです。

goog.provide('cheesepie.async.Deferred');

goog.require('goog.array');
goog.require('goog.async.Deferred');

/**
  * @param {Function=} opt_canceller
  * @param {Object=} opt_defaultScope
  * @constructor
  * @extends {goog.async.Deferred}
  */
cheesepie.async.Deferred = function(opt_canceller, opt_defaultScope) {
    goog.base(this, opt_canceller, opt_defaultScope);
};
goog.inherits(cheesepie.async.Deferred, goog.async.Deferred);

/**
  * @param {Function} canceller
  */
cheesepie.async.Deferred.prototype.setCanceller = function(canceller) {
    this.canceller_ = canceller;
};

/**
  * Asynchronized loop action.
  *
  * @param {Array} iterable The array or arrayLike object.
  * @param {Function} callback The action.
  * @param {number=} opt_wait The wait time until fire next callback.
  * @param {Object=} opt_defaultScope The default scope to call callbacks with.
  * @return {cheesepie.async.Deferred} deferred The deferred instance that is last of deferred chain.
  */
cheesepie.async.Deferred.prototype.forEach = function(iterable, callback, opt_wait, opt_defaultScope) {
    var newDeferred = null;
    var lastDeferred = null;
    var wait = opt_wait || 10;

    // Build recursion deferred chain.
    var deferredSequece = function(currentDeferred, arr, idx, callback, opt_defaultScope) {
        var nextDeferred = new cheesepie.async.Deferred(null, opt_defaultScope);
        var item = arr[idx];
        nextDeferred.addCallback(goog.bind(callback, opt_defaultScope, item, idx));
            currentDeferred.addCallback(function() {
            setTimeout(function() {
                nextDeferred.callback();           
            }, wait);
        });
        if (arr.length - 1 > idx) {
            deferredSequece(nextDeferred, arr, ++idx, callback, opt_defaultScope);
        } else {
            lastDeferred = nextDeferred;
    };
	    
    newDeferred = new cheesepie.async.Deferred(null, opt_defaultScope);
    if (goog.isArray(iterable) && iterable.length > 0) {
        deferredSequece(newDeferred, goog.array.clone(iterable), 0, callback, opt_defaultScope);
    }

    var timerId = null; 	 	 
    var cb = function() {
        timerId = setTimeout(function() {
            newDeferred.callback();
        }, wait);
    };
    newDeferred.setCanceller(function() {
        try {
            clearTimeout(timerId);
        } catch (ex) {
             // pass
        }
    });
    // Add in original chain
    this.addCallbacks(cb, null, opt_defaultScope);
    
    return lastDeferred;
};


使い方は他のコールバック追加と変わらないです。

var arr = [];
var deferred = new cheesepie.async.Deferred();
deferred.addCallback(function(res) {
    console.log(res); // "start"
    arr.push("foo");
    arr.push("bar");
})
.forEach(arr, function(item, idx) {
    console.log(idx, ": ", item); // 100ミリ秒ごとに実行される
}, 100);
.addCallback(function() {
    console.log("finish"); // ループ終了後実行される
});

// fire
deferred.callback("start");


これをなんで作ったかというと、データ行の数が多くて描画時間が数秒かかってしまう(特にIE)UIがあって、他のUIをその間ブロックしてしまう箇所がありまして。
ページャや「もっと読む」を付ければいいじゃんというのもあるのですが、業務アプリ系だとデータがすべて一覧できないと意味が無い場面もあります。
そこで、データ行の描画を後回しにして他のUIの表示/入力を妨げないようにしたというわけです。
実際にデータ行が最後の行まですべて表示されるまでの時間はブロックしちゃう版と変わらないのですが、他のUIをブロックしないだけで体感速度が劇的に違います。
まさかそんなことやらなきゃならないようなもの作る日が来るとはという感じですが、改めてDeferredパターンの素晴らしさを実感できました。


実装の雰囲気は結構違ってますが、この辺のやり方はJSDeferredのソースを読んで勉強しました。
それにしてもムズい!ソースは短いのに理解するのに半日くらい費やしましたよorz モダンへの道は険しい。
ああいうコードをさらで書けるようになりたいなあ。