jQuery之Deferred源碼剖析

來源:互聯網
上載者:User

標籤:script   執行   訂閱   移除   defer   callback   any   time   sim   

一、前言

大約在夏季,我們談過ES6的Promise(詳見here),其實在ES6前jQuery早就有了Promise,也就是我們所知道的Deferred對象,宗旨當然也和ES6的Promise一樣,通過鏈式調用,避免層層嵌套,如下:

//jquery版本大於1.8function 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源碼,如果你瞭解設計模式中的發布訂閱者模式,不難發現,就是一個”自訂事件”嘛。(詳見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;    }}

隨後聲明的一個Null 物件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-done1console.log(‘xxxxxxxxxxxxxxxxxxxx‘);then1.fire();//=>self2-done1
五、拓展閱讀

[1]、細說Promise

[2]、JavaScript之自訂事件

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

jQuery之Deferred源碼剖析

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.