使用 Deferred
在 jQuery 中,實現觀察者模式的就是 Deferred 了,我們先看它的使用。你也可以直接看 jQuery 的 Deferred 文檔。
這個對象提供了主題和訂閱的管理,使用它可以很容易實現一次性的觀察者模式。
// 定義主題 var subject = (function(){ var dfd = $.Deferred(); return dfd; })(); // 兩個觀察者 var fn1 = function(content){ console.log("fn1: " + content ); } var fn2 = function(content){ console.log("fn2: " + content ); } // 註冊觀察者 $.when( subject ) .done( fn1 ) .done( fn2 ); // 發佈內容 subject.resolve("Alice");
通常我們在主題內部來決定什麼時候,以及發佈什麼內容,而不允許在主題之外發布。通過 Deferred 對象的 promise 方法,我們可以只允許在主題之外註冊觀察者,有點像 .NET 中 event 的處理了。這樣,我們的代碼就成爲下面的形式。
// 定義主題 var subject = (function(){ var dfd = $.Deferred(); var task = function() { // 發佈內容 dfd.resolve("Alice"); } setTimeout( task, 3000); return dfd.promise(); })(); // 兩個觀察者 var fn1 = function(content){ console.log("fn1: " + content ); } var fn2 = function(content){ console.log("fn2: " + content ); } // 註冊觀察者 $.when( subject ) .done( fn1 ) .done( fn2 );
在 jQuery 中,甚至可以提供兩個主題同時被觀察, 需要注意的是,要等兩個主題都觸發之後,纔會真正觸發,每個觀察者一次得到這兩個主題,所以參數變成了 2 個。
// 定義主題 var subjectAlice = (function(){ var dfd = $.Deferred(); var task = function() { // 發佈內容 dfd.resolve("Alice"); } setTimeout( task, 3000); return dfd.promise(); })(); var subjectTom = (function(){ var dfd = $.Deferred(); var task = function() { // 發佈內容 dfd.resolve("Tom"); } setTimeout( task, 1000); return dfd.promise(); })(); // 兩個觀察者 var fn1 = function(content1, content2){ console.log("fn1: " + content1 ); console.log("fn1: " + content2 ); } var fn2 = function(content1, content2){ console.log("fn2: " + content1 ); console.log("fn2: " + content2 ); } // 註冊觀察者 $.when( subjectAlice, subjectTom ) .done( fn1 ) .done( fn2 );
實際上,在 jQuery 中,不僅可以發佈成功完成的事件,主題還可以發佈其它兩種事件:失敗和處理中。
失敗事件,通過調用主題的 reject 方法可以發佈失敗的消息,對於觀察者來說,需要通過 fail 來註冊這個事件了。
處理中事件,通過調用主題的 notify 來發布處理中的消息,對於觀察者來說,需要通過 progress 來註冊這個事件。
要是觀察者想一次性註冊多個事件,那麼,可以通過 then 來註冊,這種方式可以處理主題的成功、失敗和處理中三種事件。
$.get( "test.php" ).then( function() { alert( "$.get succeeded" ); }, function() { alert( "$.get failed!" ); } );
只考慮成功和失敗的話,就通過 always 來處理。
$.get( "test.php" ) .always(function() { alert( "$.get completed with success or error callback arguments" ); });
jQuery 中 Deferred 的使用
常用的是 ajax, get, post 等等 Ajax 函數了。它們內部都已經實現爲了 Deferred ,返回的結果就是 Deferred 對象了。也就是說你只管寫觀察者就可以了,主題內部已經處理好了,比如當 ajax 成功之後調用 resolve 來觸發 done 事件並傳遞參數的問題。你可以繼續使用傳統的回調方式,顯然推薦你使用 Deferred 方式了。這樣你的代碼結構更加清晰。
$.post( "test.php", { name: "John", time: "2pm" }) .done(function( data ) { alert( "Data Loaded: " + data ); });
如果需要訪問兩個 Ajax ,則可以這樣
$.when( $.post( "test.php", { name: "John", time: "2pm" }), $.post( "other.php" ) ) .done(function( data1, data2 ) { alert( "Data Loaded: " + data1 ); alert( "Data Loaded: " + data2 ); });
實現 Deferred
通過前面的使用,其實你一定可以想到,在 Deferred 這個對象的內部,必須有三個回調隊列了,這裏的成功和失敗只能一次完成,所以這兩個 Callbacks 都使用了 once 來定義。
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 就是直接註冊了兩個事件。then 允許我們一次處理三種註冊。
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 action = tuple[ 0 ], 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[ action + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments ); } }); }); fns = null; }).promise(); },
deferred 就是一個對象。pipe 是已經過時的用法,是 then 的別名。
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 是一個助手方法,支持多個主題。
// Deferred helper when: function( subordinate /* , ..., subordinateN */ ) { var i = 0, resolveValues = core_slice.call( arguments ), length = resolveValues.length, // the count of uncompleted subordinates remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0, // the master Deferred. If resolveValues consist of only a single Deferred, just use that. deferred = remaining === 1 ? subordinate : jQuery.Deferred(), // Update function for both resolve and progress values updateFunc = function( i, contexts, values ) { return function( value ) { contexts[ i ] = this; values[ i ] = arguments.length > 1 ? core_slice.call( arguments ) : value; if( values === progressValues ) { deferred.notifyWith( contexts, values ); } else if ( !( --remaining ) ) { deferred.resolveWith( contexts, values ); } }; }, progressValues, progressContexts, resolveContexts; // add listeners to Deferred subordinates; treat others as resolved if ( length > 1 ) { progressValues = new Array( length ); progressContexts = new Array( length ); resolveContexts = new Array( length ); for ( ; i < length; i++ ) { if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) { resolveValues[ i ].promise() .done( updateFunc( i, resolveContexts, resolveValues ) ) .fail( deferred.reject ) .progress( updateFunc( i, progressContexts, progressValues ) ); } else { --remaining; } } } // if we're not waiting on anything, resolve the master if ( !remaining ) { deferred.resolveWith( resolveContexts, resolveValues ); } return deferred.promise(); }
完整的代碼如下所示:
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 action = tuple[ 0 ], 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[ action + "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; }, // Deferred helper when: function( subordinate /* , ..., subordinateN */ ) { var i = 0, resolveValues = core_slice.call( arguments ), length = resolveValues.length, // the count of uncompleted subordinates remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0, // the master Deferred. If resolveValues consist of only a single Deferred, just use that. deferred = remaining === 1 ? subordinate : jQuery.Deferred(), // Update function for both resolve and progress values updateFunc = function( i, contexts, values ) { return function( value ) { contexts[ i ] = this; values[ i ] = arguments.length > 1 ? core_slice.call( arguments ) : value; if( values === progressValues ) { deferred.notifyWith( contexts, values ); } else if ( !( --remaining ) ) { deferred.resolveWith( contexts, values ); } }; }, progressValues, progressContexts, resolveContexts; // add listeners to Deferred subordinates; treat others as resolved if ( length > 1 ) { progressValues = new Array( length ); progressContexts = new Array( length ); resolveContexts = new Array( length ); for ( ; i < length; i++ ) { if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) { resolveValues[ i ].promise() .done( updateFunc( i, resolveContexts, resolveValues ) ) .fail( deferred.reject ) .progress( updateFunc( i, progressContexts, progressValues ) ); } else { --remaining; } } } // if we're not waiting on anything, resolve the master if ( !remaining ) { deferred.resolveWith( resolveContexts, resolveValues ); } return deferred.promise(); } });