以前自分で調べて、別のブログで書いた記事です。
自分でも仕組みを思い出すために再度投稿してみます。
Deferred
、promise
はコールバックの直列処理したいときなんかによく使ってますが、
どういう仕組みになっているのか、気になって調べてみました
なお、jQueryのバージョンは1.11.3で解説します。(この記事を書いている現在の最新版)
メソッドの名前定義とCallbacksオブジェクトとの紐づけ
Deferred
オブジェクトは、バージョン1.11.3でいうと3280行目以降に定義されています。
まず最初に、こんな配列が定義されています。
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
の直前に書かれています。
さて、以上を踏まえた上で、以下の部分を見てみましょう。
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の定義
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
オブジェクト自身に登録します。
ここがまたまたうまいコーディングされていて、分かりにくいです。
わかりやすく書き直すとこうです。
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
はそれぞれこのあと定義されています。
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
自身にコピーします。
3356行目
// Make the deferred a promise
promise.promise( deferred );
Deferred
呼び出し時に、引数に関数が指定されていた場合、その関数を実行します。このとき、この関数に自身であるdeferred
を渡します。
3359行目
// Call given func if any
if ( func ) {
func.call( deferred, deferred );
}
最後に自身であるdeferred
を返して終了です。
3364行目
// All done!
return deferred;
とりあえず1回目の読み解きはここまでにします。
deferred
に内包されたpromise
オブジェクトについては、別投稿で読み解いていきたいと思います。
最後にDeferredのコードを掲載しておきます。
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とかが定義されています。