JSDeferredを読む

前からamachangが「読むといい」って言っていたJSDeferredのコードを読む。defferedじゃなくてdeferredなので注意。

Deferred.define = function (obj, list) {
	if (!list) list = ["parallel", "wait", "next", "call", "loop"];
	if (!obj)  obj  = (function () { return this })();
	for (var i = 0; i < list.length; i++) {
		var n = list[i];
		obj[n] = Deferred[n];
	}
	return Deferred;
};

これはグローバルスコープに書き出すコード。(function () { return this })()でwindowオブジェクトがとれているみたい。

class Deferred(object):
    def define(self, obj=None, list=None):
        if not list: list = ["parallel", "wait", "next", "call", "loop"]
        if not obj: obj = globals()
        for n in list:
            obj[n] = Deferred.__dict__[n]

        return Deferred
        • -
/* function next (fun) //=> Deferred
 *
 * `next` is shorthand for creating new deferred which
 * is called after current queue.
 */
Deferred.next = function (fun) {
	var d = new Deferred();
	var id = setTimeout(function () { clearTimeout(id); d.call() }, 0);
	if (fun) d.callback.ok = fun;
	d.canceller   = function () { try { clearTimeout(id) } catch (e) {} };
	return d;
};

setTimeout(function(){alert(1)}, 1000)とやると1秒後にalertが起きるようだ。setTimeoutは数値を返してきて、clearTimeoutでスケジュールされた関数の実行をキャンセルできる。
d.callとかも読まないとここだけじゃわからないな。

Deferred.prototype = {
	init : function () {
		this._next    = null;
		this.callback = {
			ok: function (x) { return x },
			ng: function (x) { throw  x }
		};
		return this;
	},

	next  : function (fun) { return this._post("ok", fun) },
	error : function (fun) { return this._post("ng", fun) },
	call  : function (val) { return this._fire("ok", val) },
	fail  : function (err) { return this._fire("ng", err) },

	cancel : function () {
		(this.canceller || function () {})();
		return this.init();
	},

	_post : function (okng, fun) {
		this._next =  new Deferred();
		this._next.callback[okng] = fun;
		return this._next;
	},

	_fire : function (okng, value) {
		var self = this, next = "ok";
		try {
			value = self.callback[okng].call(self, value);
		} catch (e) {
			next  = "ng";
			value = e;
		}
		if (value instanceof Deferred) {
			value._next = self._next;
		} else {
			if (self._next) self._next._fire(next, value);
		}
		return this;
	}
};

_fireの中でself = thisしている意味がよくわからない…。

class Deferred(object):
    def define(self, obj=None, list=None):
        if not list: list = ["parallel", "wait", "next", "call", "loop"]
        if not obj: obj = globals()
        for n in list:
            obj[n] = Deferred.__dict__[n]

        return Deferred

    
    def init(self):
        self._next = None
        def ok(x):
            return x

        def ng(x):
            raise x
        
        sefl.callback = dict(ok=ok, ng=ng)
        return self

    def next(self, fun):
        return self._post("ok", fun)

    def error(self, fun):
        return self._post("ng", fun)

    def call(self, val):
        return self._fire("ok", val)

    def fail(self, err):
        return self._fire("ng", err)

    def _post(self, okng, fun):
        n = Deferred()
        n.callback[okng] = fun
        self._next = n
        return n

    def _fire(self, okng, value):
        self_ = self
        next = "ok"
        try:
            value = self.callback[okng].call(self_, value)
        except Exception, e:
            next = "ng"
            value = e

        if isinstance(value, Deferred):
            value._next = self_._next
        else:
            if self_._next:
                self_._next._fire(next, value)

    def _post(self, okng, fun):
        self._next = Deferred()
        self._next.callback[okng] = fun
        return self._next
        • -

Deferred.prototype.nextとDeferred.nextの両方があることに混乱してamachangに聞いたら、これは全然無関係だそうな。JSの場合、関数を平置きしてしまうとグローバル空間を汚染してしまうので適当なハッシュにつっこんであるだけ。Deferred.nextはFoo.nextでもなんでもいい。
モジュール単位でスコープが分かれたりしないJSでのコーディングスタイル。
Pythonではトップレベルで定義した関数がモジュール単位のスコープになるのでトップレベルで書くのが適切。

        • -

ああ。
JSDeferredは想像していたものとだいぶ違った。
next(f1).next(f2).next(f3)で実行すべきタスクを組み立てた後でまとめて実行するのかと思っていたけど、next(f1)の時点でf1は実行されるんだな。

        • -

> next(f1).next(f2).next(f3)で実行すべきタスクを組み立てた後でまとめて実行するのかと思っていたけど、next(f1)の時点でf1は実行されるんだな。

最初の next は読んでいただけた通り setTimeout をウェイト 0 で読んでいます。JS ではこのときはまだ、実行キューにその関数が入るだけで、実行されません。なので、.next(f2).next(f3) とすぐに繋げた場合、それらのチェインが構築されたあとに f1 は実行されるようになります。(自分でタスクをくみたてて自分であとから呼ぶこともできますが、普通は現在実行中の処理のあとに自動で実行してくれたほうが楽なのでこうなっています)

ああ、勘違いしてました。
下のようなコードを書いて、即座に表示が行われるので「あれ、即座だな」と思ったけども、

function foo(){
    console.log("foo!");
}

var id = setTimeout(function () { clearTimeout(id); foo() }, 0);

下のようなコードを試してみるべきだったわけですね。barが先に表示される。

function foo(){
    console.log("foo!");
}

var id = setTimeout(function () { clearTimeout(id); foo() }, 0);
console.log("bar!");

なるほどなるほど。
かってにnext(f1).next(f2).next(f3).start()的な使い方をするもんだと思いこんでました。