1. 程式人生 > >jQuery 2.0.3 原始碼分析 Deferred(最細的實現剖析,帶圖)

jQuery 2.0.3 原始碼分析 Deferred(最細的實現剖析,帶圖)

Deferred的概念請看第一篇

******************構建Deferred物件時候的流程圖**************************

**********************原始碼解析**********************

因為callback被剝離出去後,整個deferred就顯得非常的精簡

jQuery.extend({ 

    Deferred : function(){}
    
    when : function()

)}

對於extend的繼承這個東東,在之前就提及過jquery如何處理內部jquery與init相互引用this的問題

所以當jQuery.extend只有一個引數的時候,其實就是對jQuery靜態方法的一個擴充套件

我們在具體看看2個靜態方法內部都幹了些什麼

Deferred整體結構:

原始碼精簡了部分程式碼

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( /* fnDone, fnFail, fnProgress */ ) { }, // Get a promise for this deferred // If obj is provided, the promise aspect is added to the object promise: function( obj ) {} }, deferred = {}; jQuery.each( tuples, function( i, tuple ) { deferred[ tuple[0] + "With" ] = list.fireWith; }); promise.promise( deferred ); // All done! return deferred; },
  1. 顯而易見Deferred是個工廠類,返回的是內部構建的deferred物件
  2. tuples 建立三個$.Callbacks物件,分別表示成功,失敗,處理中三種狀態
  3. 建立了一個promise物件,具有state、always、then、primise方法
  4. 擴充套件primise物件生成最終的Deferred物件,返回該物件

這裡其實就是3個處理,但是有個優化程式碼的地方,就是把共性的程式碼給抽象出來,通過動態生成了

具體原始碼分析:

Deferred自身則圍繞這三個物件進行更高層次的抽象

  • 觸發回撥函式列表執行(函式名)
  • 添加回調函式(函式名)
  • 回撥函式列表(jQuery.Callbacks物件)
  • deferred最終狀態(第三組資料除外)
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") ]
    ],

這裡抽象出2組陣營:

1組:回撥方法/事件訂閱  

done,fail,progress

2組:通知方法/事件釋出    

resolve,reject,notify,resolveWith,rejectWith,notifyWith

tuples 元素集 其實是把相同有共同特性的程式碼的給合併成一種結構,然後通過一次處理

jQuery.each( tuples, function( i, tuple ) {
            var list = tuple[ 2 ],
                stateString = tuple[ 3 ];
            promise[ tuple[1] ] = list.add;
            if ( stateString ) {
                list.add(function() {
                    state = stateString;

                // [ reject_list | resolve_list ].disable; progress_list.lock
                }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock );
            }
            deferred[ tuple[0] ] = function() {
                deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments );
                return this;
            };
            deferred[ tuple[0] + "With" ] = list.fireWith;
        });

對於tuples的3條資料集是分2部分處理的

第一部分將回調函式存入

promise[ tuple[1] ] = list.add;

其實就是給promise賦予3個回撥函式

promise.done = $.Callbacks("once memory").add
promise.fail = $.Callbacks("once memory").add
promise.progressl = $.Callbacks("memory").add

如果存在deferred最終狀態

預設會預先向doneList,failList中的list新增三個回撥函式

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 );
}

*************************************************************
這裡有個小技巧

所以實際上第二個傳引數是1、0索引對調了,所以取值是failList.disable與doneList.disable

*************************************************************

通過stateString有值這個條件,預先向doneList,failList中的list新增三個回撥函式

分別是:

doneList : [changeState, failList.disable, processList.lock]
failList : [changeState, doneList.disable, processList.lock]
  • changeState 改變狀態的匿名函式,deferred的狀態,分為三種:pending(初始狀態), resolved(解決狀態), rejected(拒絕狀態)
  • 不論deferred物件最終是resolve(還是reject),在首先改變物件狀態之後,都會disable另一個函式列表failList(或者doneList)
  • 然後lock processList保持其狀態,最後執行剩下的之前done(或者fail)進來的回撥函式

所以第一步最終都是圍繞這add方法

  • done/fail/是list.add也就是callbacks.add,將回調函式存入回撥物件中

第二部分很簡單,給deferred物件擴充6個方法

最後合併promise到deferred

promise.promise( deferred );
jQuery.extend( obj, promise )

所以最終通過工廠方法Deferred構建的非同步物件帶的所有的方法了

return 內部的deferred物件了

image

由此可見我們在

var defer = $.Deferred(); //構建非同步物件

的時候,內部的物件就有了4個屬性方法了

  1. deferred: Object
    1. always: function () {
    2. done: function () {
    3. fail: function () {
    4. notify: function () {
    5. notifyWith: function ( context, args ) {
    6. pipe: function ( /* fnDone, fnFail, fnProgress */ ) {
    7. progress: function () {
    8. promise: function ( obj ) {
    9. reject: function () {
    10. rejectWith: function ( context, args ) {
    11. resolve: function () {
    12. resolveWith: function ( context, args ) {
    13. state: function () {
    14. then: function ( /* fnDone, fnFail, fnProgress */ ) {
  2. promise: Object
    1. always: function () {
    2. done: function () {
    3. fail: function () {
    4. pipe: function ( /* fnDone, fnFail, fnProgress */ ) {
    5. progress: function () {
    6. promise: function ( obj ) {
    7. state: function () {
    8. then: function ( /* fnDone, fnFail, fnProgress */ ) {
  3. state: "pending"
  4. tuples: Array[3]

構造圖

以上只是在初始化構建的時候,我們往下看看動態執行時候的處理

*****************執行期***********************

一個最簡單的demo為例子

var d = $.Deferred();

 setTimeout(function(){
        d.resolve(22)
  },0);

 d.then(function(val){
      console.log(val);
 })
當延遲物件被 resolved 時,任何通過 deferred.then或deferred.done 新增的 doneCallbacks,都會被呼叫。回撥函式的執行順序和它們被新增的順序是一樣的。傳遞給 deferred.resolve() 的 args 引數,會傳給每個回撥函式。當延遲物件進入 resolved 狀態後,再新增的任何 doneCallbacks,當它們被新增時,就會被立刻執行,並帶上傳入給 .resolve()的引數

換句話說,我們呼叫d.resolve(22) 就等於是呼叫

匿名函式並傳入引數值 22

function(val){
      console.log(val); //22
 }

當前實際的使用中會有各種複雜的組合情況,但是整的外部呼叫流程就是這樣的

***************** resolve的實現 *******************

我們回顧下,其實Deferred物件,內部的實現還是Callbacks物件,只是在外面再封裝了一層API,供介面呼叫

d.resolve(22)

實際上呼叫的就是通過這個程式碼生成的

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

最終執行的就是 list.fireWith

callbacks.fireWith()

所以最終又回到回撥物件callbacks中的私有方法fire()了

Callbacks會通過

把回撥函式給註冊到內部的list = []上,我們回來過看看

d.then(function(val){
      console.log(val);
 })

***************** then的實現 *******************

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() {
                   //省略............
            });
        });
        fns = null;
    }).promise();
},
  • 遞迴jQuery.Deferred
  • 傳遞了func
  • 鏈式呼叫了promise()

因為在非同步物件的方法都是巢狀找作用域屬性方法的

這裡我額外的提及一下作用域

var d = $.Deferred();

這個非同步物件d是作用域是如何呢?

第一層:無可爭議,瀏覽器環境下最外層是 window

第二層:jquery本身是一個閉包

第三層: Deferred工廠方法產生的作用域

如果用d.then()方法呢?

很明顯then方法又是巢狀在內部的函式,所以執行的時候都預設會包含以上三層作用域+自己本身函式產生的作用域了

我們用個簡單圖描繪下

簡報1

根據規則,在最內部的函式能夠訪問上層作用域的所有的變數

我們先從使用的層面去考慮下結構設計:

demo 1

var defer = $.Deferred();

  var filtered = defer.then(function( value ) {
        return value * 2;
      });

  defer.resolve( 5 );

  filtered.done(function( value ) {
      console.log(value) //10
  });

demo 2

var defer = $.Deferred();

  defer.then(function(value) {
    return value * 2;
  }).then(function(value) {
    return value * 2;
  }).done(function(value) {
      alert(value)  //20
  });

  defer.resolve( 5 );

其實這裡就是涉及到defer.then().then().done()  鏈式呼叫了

API是這麼定義的:

deferred.then( doneFilter [, failFilter ] [, progressFilter ] )
從jQuery 1.8開始, 方法返回一個新的promise(承諾),通過一個函式,可以過濾deferred(延遲)的狀態和值。替換現在過時的deferred.pipe()方法。 doneFilter 和 failFilter函式過濾原deferred(延遲)的解決/拒絕的狀態和值。 progressFilter 函式過濾器的任何呼叫到原有的deferred(延遲)的notify 和 notifyWith的方法。 這些過濾器函式可以返回一個新的值傳遞給的 promise(承諾)的.done() 或 .fail() 回撥,或他們可以返回另一個觀察的物件(遞延,承諾等)傳遞給它的解決/拒絕的狀態和值promise(承諾)的回撥。 如果過濾函式是空,或沒有指定,promise(承諾)將得到與原來值相同解決(resolved)或拒絕(rejected)。

我們抓住幾點:

  • 返回的是新的promise物件
  • 內部有一個濾器函式

從demo 1中我們就能看到

經過x.then()方法處理的程式碼中返回的this(filtered ),不是原來的$.Deferred()所有產生的那個非同步物件(defer )了

所以,每經過一個then那麼內部處理的this都要被重新設定,那麼為什麼要這樣處理呢?

原始碼

then: function( /* fnDone, fnFail, fnProgress */ ) {
                    var fns = arguments;
                    //分別為deferred的三個callbacklist添加回調函式,根據fn的是否是函式,分為兩種情況
                    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傳遞實參的時候,支援一個flag,jQuery.Deferred(func)

傳遞一個回撥函式

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

所以newDefer可以看作是

newDefer = $.Deferred();

那麼func回撥的處理的就是過濾函數了

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 );
    }
});

這裡其實也有編譯函式的概念,講未來要執行的程式碼,預先通過閉包函式也儲存起來,使其訪問各自的作用域

第一步

分解tuples元素集

jQuery.each( tuples, function( i, tuple ) {
   //過濾函式第一步處理
})

第二步

分別為deferred[ done | fail | progress ]執行對應的add方法,增加過濾函式給done | fail | progress 方法

deferred[ tuple[1] ](
傳入過濾函式
)//過濾函式 執行的時候在分解

程式碼即

deferred[done] = list.add = callback.add

第三步

返回return jQuery.Deferred().promise()

此時構建了一個新的Deferred物件,但是返回的的是經過promise()方法處理後的,返回的是一個受限的promise物件

所以整個then方法就處理了2個事情

  • 構建一個新的deferred物件,返回受限的promise物件
  • 給父deferred物件的[ done | fail | progress ]方法都增加一個過濾函式的方法

我們知道defer.then方法返回的是一個新的jQuery.Deferred().promise()物件

那麼我們把defer.then返回的稱之為子物件,那麼如何與父物件var defer = $.Deferred() 關聯的起來的

我看看原始碼

deferred[ tuple[1] ](//過濾函式//)

deferred其實就是根級父物件的引用,所以就巢狀再深,其實都是呼叫了父物件deferred[ done | fail | progress 執行add罷了

從圖中就能很明顯的看到 2個不同的deferred物件中 done fail progress分別都儲存了不同的處理回調了

deferred.resolve( args )
  • 當延遲物件被 resolved 時,任何通過 或deferred.done 新增的 doneCallbacks,都會被呼叫
  • 回撥函式的執行順序和它們被新增的順序是一樣的
  • 傳遞給 deferred.resolve()args 引數,會傳給每個回撥函式
  • 當延遲物件進入 resolved 狀態後,再新增的任何 doneCallbacks,當它們被新增時,就會被立刻執行,並帶上傳入給.resolve()的引數

流程如圖

流程解析:

1 執行fire()方法,遞迴執行list所有包含的處理方法

2 執行了預設的 changeState, disable, lock 方法、

3 執行過濾函式

      根據 var returned = fn.apply( this, arguments )的返回值(稱作returnReferred)是否是deferred物件

  • 返回值是deferred物件,那麼在returnReferred物件的三個回撥函式列表中新增newDeferred的resolve(reject,notify)方法,也就是說newDeferrred的執行依賴returnDeferred的狀態
  • 不是函式的情況(如值為undefined或者null等),直接連結到newDeferred的resolve(reject,notify)方法,也就是說  newDeferrred的執行依賴外層的呼叫者deferred的狀態或者說是執行動作(resolve還是reject或者是notify)  此時deferred.then()相當於將自己的callbacklist和newDeferred的callbacklist連線起來

image

下面就是巢狀deferred物件的劃分了

image

原始碼還是要靠自己去折騰的

思想的提高比較難的,我們可以借鑑設計的思路,程式碼書寫方式都是有益無害的

流程的分析已經比較透徹了,下一章在講解when的實現

寫這東西太耗精力了,如果對您有幫助,請點選推薦支援一下……………