5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

jQueryのDeferredを読み解く 1

Last updated at Posted at 2015-06-02

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

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

なお、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,notifyfireメソッドのエイリアスとなるのです。
というわけで、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オブジェクトを保持しておきます。
statesStringresolved,rejectedを保持しておきます。
次にpromiseオブジェクトのdone,fail,progresscallbacksオブジェクトの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を実行->callbackfireWith(=resolveWith)が実行される->callbackadd(done)した関数が実行される。

rejectを実行->callbackfireWith(=rejectWith)が実行される->callbackadd(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とかが定義されています
5
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?