jQuery之Deferred源碼剖析

来源:http://www.cnblogs.com/giggle/archive/2016/12/19/6192913.html
-Advertisement-
Play Games

通過簡單的示例代碼,短暫地回顧了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之自定義事件

[3]、JavaScript之鏈式結構序列化


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、Angular特點: 1、雙向數據綁定,主打賣點 2、MVVM 模型,把視圖和邏輯分開 3、依賴註入 個人感覺,在Angular中,視圖對應 HTML 模板,視圖邏輯對應directive 和 controller。 二、模塊 Angular 中通過模塊來管理命名空間,可以通過不同的模塊來隔離不 ...
  • 本文轉載自 http://www.mynawang.com/ 相關代碼可訪問 https://github.com/mynawang/Design-Pattern-Learning 什麼是設計模式 設計模式(Design pattern)是一套被反覆使用、多數人知曉的、經過分類編目的、代碼設計經驗的 ...
  • <html> ...
  • 簡介 "transformjs" 在非react領域用得風生水起,那麼react技術棧的同學能用上嗎?答案是可以的。junexie童鞋已經造了個 "react版本" 。 動畫實現方式 傳統 web 動畫的兩種方式 : 1. 純粹的CSS3 :如:transition/animation+transf ...
  • 基本上每個項目都需要用到模態框組件,由於在最近的項目中,alert組件和confirm是兩套完全不一樣的設計,所以我將他們分成了兩個組件,本文主要討論的是confirm組件的實現。 ...
  • 一、AJAX示例 AJAX全稱為“Asynchronous JavaScript And XML”(非同步JavaScript和XML) 是指一種創建互動式網頁應用的開發技術、改善用戶體驗,實現無刷新效果。 1.1、優點 不需要插件支持 優秀的用戶體驗 提高Web程式的性能 減輕伺服器和帶寬的負擔 1 ...
  • 一、插入換行 ~:表示同輩元素之後指定類型的元素,如;elm1 ~ elm2表示,elm1之後的所有elm2元素,且elm1與elm2都是在同一個父級元素。 +:表示同輩元素的兄弟元素。 \A:一個空白換行符 解決方案: 在dd與dt相鄰的dt上的子元素前面加入一個空白換行符 在相鄰的dd與dd之間 ...
  • 環境:win7 64位 IE9 錯誤:SCRIPT5011:不能執行已釋放Script的代碼。 現象:在父窗體的close()中調用嵌套的iframe頁面的js方法返回一個對象時拋此異常。 原因:在一個iframe中定義對象,在這個iframe被刪除後,方法執行的環境丟失,就會報這個錯誤 我的解決辦 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...