jQueryのDeferredを読み解く 1

以前自分で調べて、別のブログで書いた記事です。
自分でも仕組みを思い出すために再度投稿してみます。

`Deferred`、`promise`はコールバックの直列処理したいときなんかによく使ってますが、
どういう仕組みになっているのか、気になって調べてみました

なお、jQueryのバージョンは1.11.3で解説します。(この記事を書いている現在の最新版)

#メソッドの名前定義とCallbacksオブジェクトとの紐づけ
`Deferred`オブジェクトは、バージョン1.11.3でいうと3280行目以降に定義されています。
まず最初に、こんな配列が定義されています。

````jquery-1.11.3.js
3282行目
	Deferred: function( func ) {
		var tuples = [
				// action, add listener, listener list, final state
				[ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ],
				[ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ],
				[ "notify", "progress", jQuery.Callbacks("memory") ]
			],
			state = "pending",
````

すでにDefferedを使っている人にはお馴染みの名前ですよね。
`resolve`を実行すると、`done`に登録した関数が実行され、`deferred`のステータスは`resolved`になります。
`rejecte`を実行すると、`fail`に登録した関数が実行され、`deferred`のステータスは`rejected`となります。
`notify`を実行すると、`progress`に登録した関数が実行されます。このときステータスは変わりません。初期値である`pending`のまま

3番目の要素には、jQueryの`Callbacks`オブジェクトが生成されています。
この記事では`Callback`オブジェクトの詳細には触れませんが、この`Callbacks`オブジェクトが重要な役割をになっています。

`done`,`fail`,`progress`がそれぞれ`Callbacks`オブジェクトの`add`メソッドのエイリアスとなり、
`resolve`,`reject`,`notify`が`fire`メソッドのエイリアスとなるのです。
というわけで、`Callbacks`オブジェクトの使い方を理解していないと、`Deferred`のコードを理解するのは困難です。ちなみに`Callbacks`オブジェクトのソースコードは、`Deferred`の直前に書かれています。

さて、以上を踏まえた上で、以下の部分を見てみましょう。

````jquery-1.11.3.js
 3331行目
// Add list-specific methods
jQuery.each( tuples, function( i, tuple ) {
        var list = tuple[ 2 ],
                stateString = tuple[ 3 ];

        // promise[ done | fail | progress ] = list.add
        promise[ tuple[1] ] = list.add;

        // Handle state
        if ( stateString ) {
                list.add(function() {
                        // state = [ resolved | rejected ]
                        state = stateString;

                // [ reject_list | resolve_list ].disable; progress_list.lock
                }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock );
        }

        // deferred[ resolve | reject | notify ]
        deferred[ tuple[0] ] = function() {
                deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments );
                return this;
        };
        deferred[ tuple[0] + "With" ] = list.fireWith;
});
````


配列`tuples`の要素が順番に処理されます。

まず、`list`変数に`callbacks`オブジェクトを保持しておきます。
`statesString`に`resolved`,`rejected`を保持しておきます。
次に`promise`オブジェクトの`done`,`fail`,`progress`に`callbacks`オブジェクトのaddを割り当てます。
`deferred`ではなく、`promise`オブジェクトに割り当てているところがミソなのですが、とりあえずここではそういうものだと思っておけばOKです。

次も実にうまいコーティングがされていて一見わかりにくいです。書き換え(展開)してみます。
#done,failの定義
````jquery-1.11.3.js
3339行目
if ( stateString ) {
        list.add(function() {
                // state = [ resolved | rejected ]
                state = stateString;

        // [ reject_list | resolve_list ].disable; progress_list.lock
        }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock );
}

この部分を書き換えると

//doneメソッドの部分(jQuery.eachの1回目)
list.add(function(){
state="resolved";
),tuples[1][2].disable,tuples[2][2].lock);

//failメソッドの部分(jQuery.eachの2回目)
list.add(function(){
state="rejected";
),tuples[0][2].disable,tuples[2][2].lock);

````

解説すると、
`done`メソッドの部分
1.`resolve`した場合には、ステータスを`resolved`に変更
2.`fail`が登録された`callback`オブジェクトを無効化
3.`progress`の実行も無効化

`fail`メソッドの部分
ます。
1.`reject`した場合には、ステータスを`rejected`に変更
2.`done`が登録された`callback`オブジェクトを無効化
3.`progress`の実行も無効化

要は1度`resolve`したらもう`reject`はできないよー
1度`reject`したらもう`resolve`はできないよー
という武士に二言は無い的な取り決めをここでしているわけです。

#resolve,rejectedの定義

次に`resolved`,`rejected`関数を定義しています。
これらは`promise`ではなく、`deferred`オブジェクト自身に登録します。
ここがまたまたうまいコーディングされていて、分かりにくいです。
わかりやすく書き直すとこうです。

````jquery-1.11.3.js
3348行目
// deferred[ resolve | reject | notify ]
deferred[ tuple[0] ] = function() {
        deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments );
        return this;
};
deferred[ tuple[0] + "With" ] = list.fireWith;

書き直すと

deferred["reject"]=function(){
If(this===deferred){
    deferred["rejectWith"](promise,arguments);
}else{
    deferred["rejectWith"](this,arguments);
}
````
これを見ると、`resolveWith`,`rejectWith`関数を呼んでいるみたいですね。

#resolveWith,rejectWithの定義
`resolveWith`,`rejectWith`はそれぞれこのあと定義されています。

````jquery-1.11.3.js
3353行目
deferred[tuple[0] + "With"]  = list.fireWith;

書き変えると
//1回目のループ
deferred["resolveWith"]  = list.fireWith;
//2回目のループ
deferred["rejectWith"]  = list.fireWith;
````

これで`callback`オブジェクトに結びつけることができました。

処理をおさらいすると
`resolve`を実行->`callback`の`fireWith(=resolveWith)`が実行される->`callback`に`add(done)`した関数が実行される。

`reject`を実行->`callback`の`fireWith(=rejectWith)`が実行される->`callback`に`add(fail)`した関数が実行される。
という処理ができるようになったのです。
僕みたいな素人シュミグラマーには、これをコーディングした人はすごいと思います。神。

次に、`promise`オブジェクトを`deferred`自身にコピーします。

````jquery-1.11.3.js
3356行目
// Make the deferred a promise
promise.promise( deferred );
````
 `Deferred`呼び出し時に、引数に関数が指定されていた場合、その関数を実行します。このとき、この関数に自身である`deferred`を渡します。

```jquery-1.11.3.js
3359行目
// Call given func if any
if ( func ) {
       func.call( deferred, deferred );
}
````
最後に自身である`deferred`を返して終了です。

````jquery-1.11.3.js
3364行目
// All done!
return deferred;
````
とりあえず1回目の読み解きはここまでにします。
`deferred`に内包された`promise`オブジェクトについては、別投稿で読み解いていきたいと思います。


最後にDeferredのコードを掲載しておきます。

````jquery-1.11.3.js
3280行目
jQuery.extend({

Deferred: function( func ) {
        var tuples = [
                        // action, add listener, listener list, final state
                        [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ],
                        [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ],
                        [ "notify", "progress", jQuery.Callbacks("memory") ]
                ],
                state = "pending",
                promise = {
                        state: function() {
                                return state;
                        },
                        always: function() {
                                deferred.done( arguments ).fail( arguments );
                                return this;
                        },
                        then: function( /* fnDone, fnFail, fnProgress */ ) {
                                var fns = arguments;
                                return jQuery.Deferred(function( newDefer ) {
                                        jQuery.each( tuples, function( i, tuple ) {
                                                var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ];
                                                // deferred[ done | fail | progress ] for forwarding actions to newDefer
                                                deferred[ tuple[1] ](function() {
                                                        var returned = fn && fn.apply( this, arguments );
                                                        if ( returned && jQuery.isFunction( returned.promise ) ) {
                                                                returned.promise()
                                                                        .done( newDefer.resolve )
                                                                        .fail( newDefer.reject )
                                                                        .progress( newDefer.notify );
                                                        } else {
                                                                newDefer[ tuple[ 0 ] + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments );
                                                        }
                                                });
                                        });
                                        fns = null;
                                }).promise();
                        },
                        // Get a promise for this deferred
                        // If obj is provided, the promise aspect is added to the object
                        promise: function( obj ) {
                                return obj != null ? jQuery.extend( obj, promise ) : promise;
                        }
                },
                deferred = {};

        // Keep pipe for back-compat
        promise.pipe = promise.then;

        // Add list-specific methods
        jQuery.each( tuples, function( i, tuple ) {
                var list = tuple[ 2 ],
                        stateString = tuple[ 3 ];

                // promise[ done | fail | progress ] = list.add
                promise[ tuple[1] ] = list.add;

                // Handle state
                if ( stateString ) {
                        list.add(function() {
                                // state = [ resolved | rejected ]
                                state = stateString;

                        // [ reject_list | resolve_list ].disable; progress_list.lock
                        }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock );
                }

                // deferred[ resolve | reject | notify ]
                deferred[ tuple[0] ] = function() {
                        deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments );
                        return this;
                };
                deferred[ tuple[0] + "With" ] = list.fireWith;
        });

        // Make the deferred a promise
        promise.promise( deferred );

        // Call given func if any
        if ( func ) {
                func.call( deferred, deferred );
        }

        // All done!
        return deferred;
},
この後にwhenとかが定義されています。
````