通過簡單的示例代碼,短暫地回顧了jQuery的Deferred使用後,我們一起來看看jQuery是怎麼實現Deferred,當然解讀jQuery源代碼的版本是大於1.8的。 ...
一、前言 |
大約在夏季,我們談過ES6的Promise(詳見here),其實在ES6前jQuery早就有了Promise,也就是我們所知道的Deferred對象,宗旨當然也和ES6的Promise一樣,通過鏈式調用,避免層層嵌套,如下:
//jquery版本大於1.8 function runAsync(){ var def = $.Deferred(); setTimeout(function(){ console.log('I am done'); def.resolve('whatever'); }, 1000); return def; } runAsync().then(function(msg){ console.log(msg);//=>列印'whatever' }).done(function(msg){ console.log(msg);//=>列印'undefined' });
註:從jQuery1.8版本開始,then方法會返回一個新的受限制的deferred對象,即deferred.promise()—後續源碼解讀中我們會更加全面地瞭解到。因此,上述代碼done中會列印’undefined’。
好了,通過上述示例代碼,短暫的回顧了jQuery的Deferred使用後,我們一起來看看jQuery是怎麼實現Deferred,當然解讀jQuery的版本是大於1.8。
二、jQuery之Deferred源碼剖析 |
整體架構,如下:
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() {...}, always: function() {...}, then: function() {...}, promise: function( obj ) {...} }, deferred = {}; // Keep pipe for back-compat promise.pipe = promise.then; // Add list-specific methods jQuery.each( tuples, function( i, tuple ) {} ); // Make the deferred a promise promise.promise( deferred ); // Call given func if any if ( func ) { func.call( deferred, deferred ); } // All done! return deferred; } }
整體架構上,如果你瞭解設計模式中的工廠模式,那麼不難看出,jQuery.Deferred就是一個工廠,每次執行jQuery.Deferred時,都會返回一個加工好的deferred對象。
接下來,我們再一步一步剖析上述代碼。
首先,是數組tuples:
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" ) ] ]
tuples一開始就為我們預先定義了三種狀態—‘resolved’、’rejected’以及’pending’,以及它們所對應的一系列值和操作,值得註意的是每種狀態中,都調用了一個jQuery.Callbacks方法,如下:
它是個什麼玩意兒?
jQuery.Callbacks = function( options ) { // Convert options from String-formatted to Object-formatted if needed // (we check in cache first) options = typeof options === "string" ? createOptions( options ) : jQuery.extend( {}, options ); var // Flag to know if list is currently firing firing, // Last fire value for non-forgettable lists memory, // Flag to know if list was already fired fired, // Flag to prevent firing locked, // Actual callback list list = [], // Queue of execution data for repeatable lists queue = [], // Index of currently firing callback (modified by add/remove as needed) firingIndex = -1, // Fire callbacks fire = function() { // Enforce single-firing locked = options.once; // Execute callbacks for all pending executions, // respecting firingIndex overrides and runtime changes fired = firing = true; for ( ; queue.length; firingIndex = -1 ) { memory = queue.shift(); while ( ++firingIndex < list.length ) { // Run callback and check for early termination if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false && options.stopOnFalse ) { // Jump to end and forget the data so .add doesn't re-fire firingIndex = list.length; memory = false; } } } // Forget the data if we're done with it if ( !options.memory ) { memory = false; } firing = false; // Clean up if we're done firing for good if ( locked ) { // Keep an empty list if we have data for future add calls if ( memory ) { list = []; // Otherwise, this object is spent } else { list = ""; } } }, // Actual Callbacks object self = { // Add a callback or a collection of callbacks to the list add: function() { if ( list ) { // If we have memory from a past run, we should fire after adding if ( memory && !firing ) { firingIndex = list.length - 1; queue.push( memory ); } ( function add( args ) { jQuery.each( args, function( _, arg ) { if ( jQuery.isFunction( arg ) ) { if ( !options.unique || !self.has( arg ) ) { list.push( arg ); } } else if ( arg && arg.length && jQuery.type( arg ) !== "string" ) { // Inspect recursively add( arg ); } } ); } )( arguments ); if ( memory && !firing ) { fire(); } } return this; }, // Remove a callback from the list remove: function() { jQuery.each( arguments, function( _, arg ) { var index; while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { list.splice( index, 1 ); // Handle firing indexes if ( index <= firingIndex ) { firingIndex--; } } } ); return this; }, // Check if a given callback is in the list. // If no argument is given, return whether or not list has callbacks attached. has: function( fn ) { return fn ? jQuery.inArray( fn, list ) > -1 : list.length > 0; }, // Remove all callbacks from the list empty: function() { if ( list ) { list = []; } return this; }, // Disable .fire and .add // Abort any current/pending executions // Clear all callbacks and values disable: function() { locked = queue = []; list = memory = ""; return this; }, disabled: function() { return !list; }, // Disable .fire // Also disable .add unless we have memory (since it would have no effect) // Abort any pending executions lock: function() { locked = true; if ( !memory ) { self.disable(); } return this; }, locked: function() { return !!locked; }, // Call all callbacks with the given context and arguments fireWith: function( context, args ) { if ( !locked ) { args = args || []; args = [ context, args.slice ? args.slice() : args ]; queue.push( args ); if ( !firing ) { fire(); } } return this; }, // Call all the callbacks with the given arguments fire: function() { self.fireWith( this, arguments ); return this; }, // To know if the callbacks have already been called at least once fired: function() { return !!fired; } }; return self; };代碼太長,請自行打開
細細品味了上述jQuery.Callbacks源碼,如果你瞭解設計模式中的發佈訂閱者模式,不難發現,就是一個”自定義事件”嘛。(詳見here)
所以,我們精簡jQuery.Callbacks後,核心代碼如下:
jQuery.Callbacks = function(){ var list = [], self = { add: function(){/*添加元素到list*/}, remove: function(){/*從list移除指定元素*/}, fire: function(){/*遍歷list並觸發每次元素*/} }; return self; }
一目瞭然,我們每執行一次jQuery.Callbacks方法,它就會返回一個獨立的自定義事件對象。在tuples每個狀態中執行一次jQuery.Callbacks,也就豁然開朗了—為每個狀態提供一個獨立的空間來添加、刪除以及觸發事件。
好了,關於變數tuples,我們就算大致解讀完了。
state就是deferred對象的狀態值嘛,我們可以通過deferred.state方法獲取(稍後會見到)。
promise就是一個擁有state、always、then、promise方法的對象,每個方法詳解如下:
promise = { state: function() {//返回狀態值 return state; }, always: function() {//不管成功還是失敗,最終都會執行該方法 deferred.done( arguments ).fail( arguments ); return this; }, then: function( /* fnDone, fnFail, fnProgress */ ) {...},//重頭戲,稍後會詳講 promise: function( obj ) {//擴展promise,如不久我們會看見的promise.promise( deferred ); return obj != null ? jQuery.extend( obj, promise ) : promise; } }
隨後聲明的一個空對象deferred。
promise.pipe=promise.then,就不累贅了,下麵我們來看看jQuery.each(tuples, function(i, tuple){…})都幹了什麼,源碼如下:
/* tuples = [ [ "resolve", "done", jQuery.Callbacks( "once memory" ), "resolved" ], [ "reject", "fail", jQuery.Callbacks( "once memory" ), "rejected" ], [ "notify", "progress", jQuery.Callbacks( "memory" ) ] ] */ 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; } );
通過jQuery.each遍歷tuples數組,並對其進行相關操作,比如我們拿tuples數組中的第一個元素舉例:
['resolve', 'done', jQuery.Callbacks('once memory'), 'resolved']
第一步、聲明的變數list指向jQuery.Callbacks返回的對象,stateString取值為’resolved’
第二步、為promise添加’done’屬性,並指向第一步中list.add(fail和progress即指向屬於各自的自定義事件對象)
第三步、判斷stateString值,如果為’resolved’或’rejected’狀態,那麼就添加三個事件函數到對應的list列表中:
--改變state狀態的函數
--禁止對應狀態的處理,如’resolved’後,那麼必定不會觸發rejected狀態咯,反之亦然
--禁止pending狀態,都’resolved’或者’rejected’了,那麼deferred肯定不會處於pending狀態咯
第四步、為對象deferred,添加觸發各自狀態(’resolved’,’rejected’,’pending’)的fire相關方法:
--resolve、resolveWith
--reject、rejectWith
--notify、notifyWith
好了,jQuery.each(tuples, function(i, tuple){…})解讀就到此結束了。
總結:
通過jQuery.each遍歷tuples,將tuples里的三種狀態操作值done、fail以及progress添加到promise對象,並分別指向各自自定義對象中的add方法。如果狀態為resolved或rejected,那麼,再將三個特定函數添加到各自自定義對象的list列表下。隨後,就是對deferred對象賦予三個狀態各自的觸發事件啦。
至此,promise、deferred對象如下圖所示:
我們在前面講解promise對象時,提到過它的promise屬性,即為擴展promise對象,再回顧下:
所以接下來,源代碼中的promise.promise(deferred),即為擴展deferred對象,讓原來只有6個觸發屬性的deferred,同時擁有了promise對象的全部屬性。
緊接著,func.call(deferred, deferred),即為執行參數func,當然,前提是func有值。值得註意的是,是將deferred作為func的執行對象以及執行參數的,這一點在promise.then中體現得淋淋盡致(稍後會細說)。
最後$.Deferred返回構建好的deferred對象。
到此,構建deferred整體流程走完。
三、細說promise.then |
promise.then源碼如下:
promise = { 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() .progress( newDefer.notify ) .done( newDefer.resolve ) .fail( newDefer.reject ); } else { newDefer[ tuple[ 0 ] + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments ); } } ); } ); fns = null; } ).promise(); } }
精簡promise.then的源碼如下:
promise = { then: function( /* fnDone, fnFail, fnProgress */ ) { var fns = arguments; return jQuery.Deferred( function( newDefer ) { ... } ).promise(); } }
整體架構上,可以清晰的看出,promise.then方法最後通過jQuery.Deferred返回了一個新的受限制的deferred對象,即deferred.promise,正因為這樣,所以執行完then方法後,我們是不能通過deferred.pomise手動觸發resolve、reject或notify的。
接下來,我們再一步一步剖析promise.then源碼。
var fns = arguments不過就是將then方法中的參數賦予fns,在接下來的jQuery.each里使用。接著,就通過jQuery.Deferred返回了一個構建好的deferred對象,但是註意,在jQuery.Deferred里有個參數—匿名函數,還記得在上一小節末尾處,我們說過如果jQuery.Deferred里有值,就執行它,並將構建好的deferred作為執行對象和參數傳入麽:
固,promise.then方法中的newDefer指向通過jQuery.Deferred構建好的deferred。
緊接著,jQuery.each(tuples, function(i,tuple){…})處理,重點就是deferred[tuple[1]](function(){…});,註意,這裡的deferred是then方法的父deferred哦,如下:
且tuple[1]為—done|fail|progress,在前面我們已經談過,它們指向各自自定義事件對象的add方法。因此,也就明白了為什麼deferred.resolve|reject|notify後,如果隨後有then,會觸發then方法的相關事件,如下:
但是,then方法後有then方法,又是怎麼操作的呢?
它會判斷then方法中的回調函數的返回值,如果是一個deferred對象,那麼就將then方法自行創建的deferred對象中的相關觸發事件,添加到回調函數中返回的deferred對象的對應的list列表中,這樣,當我們觸發回調函數中的相關觸發事件後,也就會觸發then方法的deferred對象了,從而,如果then方法後有then方法,也就關聯了。
好了,那麼如果then方法中的回調函數的返回值是一個非deferred對象呢?那麼它就將這個返回值帶上,直接觸發then方法自行創建的deferred對象的相關事件,從而,如果then方法後有then方法,也就關聯了。
好了,promise.then方法解決就算基本完畢。
四、思考 |
細細品來,大家有沒有發現,其實promise.then就是通過作用域鏈,利用jQuery.Deferred中的變數deferred來關聯父deferred的。如果,你還記得數據結構中的單鏈表,有沒有發覺似曾相識呢,作者在這裡通過jQuery.Deferred這個工廠構建每個deferred,然後利用作用域鏈相互關聯,就如同單鏈表一樣。
因此,藉助這一思想,我們就一同模擬一個非常簡單的Deferred,稱作SimpleDef。主要作用就是每次我們執行SimpleDef函數,它都會返回一個構建好的simpleDef對象,該對象裡面包含了三個方法done、then以及fire:
--done就如同add方法般,將done里的參數添加到它父simpleDef列表list中,並返回父simpleDef對象;
--then就是將其參數func添加到父SimpleDef對象的列表list中,並返回一個新SimpleDef對象;
--fire就是觸發對應simpleDef對象的list列表裡的所有函數。
實現代碼如下:
function SimpleDef(){ var list = [], simpleDef = { done: function(func){ list.push(func); return simpleDef; }, then: function(func){ list.push(func); return SimpleDef(); }, fire: function(){ var i = list.length; while(i--){ list[i](); } } }; return simpleDef; }
測試代碼如下:
var def = SimpleDef(); var then1 = def.done(function(){ console.log('self1-done1'); }).done(function(){ console.log('self1-done2'); }).then(function(){ console.log('self2-then1'); }).done(function(){ console.log('self2-done1'); }); def.fire();//=>self2-then1 self1-done2 self1-done1 console.log('xxxxxxxxxxxxxxxxxxxx'); then1.fire();//=>self2-done1
五、拓展閱讀 |
[1]、細說Promise
[2]、JavaScript之自定義事件