1. 程式人生 > >jQuery源代碼解析(3)—— ready載入、queue隊列

jQuery源代碼解析(3)—— ready載入、queue隊列

else ng- settime eve ref promise ont 出隊 function

ready、queue放在一塊寫,沒有特殊的意思,僅僅是相對來說它倆可能源代碼是最簡單的了。ready是在dom載入完畢後。以最高速度觸發,非常實用。

queue是隊列。比方動畫的順序觸發就是通過默認隊列’fx’處理的。

(本文採用 1.12.0 版本號進行解說,用 #number 來標註行號)

ready

非常多時候,我們須要盡快的載入一個函數,假設裏面含有操作dom的邏輯,那麽最好在dom剛剛載入完畢時調用。window的load事件會在頁面中的一切都載入完畢時(圖像、js文件、css文件、iframe等外部資源)觸發,可能會因外部資源過多而過遲觸發。

DOMContentLoaded

:IE9+、Firefox、Chrome、Safari3.1+、Opera9+
html5規範指定的標準事件,在document上,在形成完整的dom樹後就會觸發(不理會圖像、js文件、css文件等是否下載完畢)。

readystatechange:IE、Firfox4+、Opera
這個事件的目的是提供與文檔或元素的載入狀態相關的信息,但這個事件的行為有時候非常難預料。支持該事件的每一個對象都有一個readyState屬性,可能包括下列5個值中的一個。

uninitialized(未初始化):對象存在但尚未初始化
loading(正在載入):對象載入數據完畢
interactive(交互):能夠操作對象了,但還沒有全然載入
complete(完畢):對象已經載入完畢

對document而言,值為”interactive”的readyState會在與DOMContentLoaded大致同樣時刻觸發readystatechange(行為難料,該階段既可能早於也可能晚於complete階段,jq上報告了一個interactive的bug,所以源代碼中用的complete)。而且在包括較少或較小的外部資源的頁面中,readystatechange有可能晚於load事件,因此優先使用DOMContentLoaded

jQuery思路

jq能夠通過$(xx).ready(fn)指定dom載入完後須要盡快調用的事件。

我們知道事件一旦錯過了監聽,就不會再觸發,$().ready()添加了遞延支持,這裏自然要使用‘once memory‘

的觀察者模型,Callback、Deferred對象均可,源代碼中是一個Deferred對象,同一時候掛載在變量readyList上。

// #3539
jQuery.fn.ready = function( fn ) {

    // jQuery.ready.promise() 為deferred對象內的promise對象(即readyList.promise())
    jQuery.ready.promise().done( fn );

    // 鏈式
    return this;
};

有了promise對象。須要dom載入完後,盡快的resolve這個promise。

推斷載入完的方式。就是首先推斷是否已經是載入完畢狀態,假設不是優先使用DOMContentLoaded事件,IE6-8用readystatechange,都要用load事件保底,保證一定觸發。因為readystatechange為complete時機詭異有時甚至慢於load。IE低版本號能夠用定時器反復document.documentElement.doScroll(‘left‘)推斷,僅僅有dom載入完畢調用該方法才不報錯。從而實現盡快的觸發。

jQuery是富有極客精神的。綁定的觸發函數調用一次後就不再實用,因此觸發函數中不僅能resolve那個promise。還會自己主動解綁觸發函數(方法detach())。這樣比方readystatechange、load多事件不會反復觸發,同一時候節省內存。

當然doScroll方法是setTimeout完畢的,假設被readystatechange搶先觸發。須要有變量能告知他取消操作,源代碼中是jQuery.isReady
觸發函數->completed() = 解綁觸發函數->detach() + resolve那個promise->jQuery.ready()

jq中添加了holdReady(true)功能,能夠延緩promise的觸發,holdReady()不帶參數(即jQuery.ready(true))則消減延遲次數,readyWait初始為1,減至0觸發。

因為doScroll靠jQuery.isReady防止反復觸發。因此即使暫緩jQuery.ready()也要能正常的設置jQuery.isReady = true

jQuery.ready()不僅能觸發promise。之後還會觸發’ready’自己定義事件。

思路整理

jQuery.fn.ready()  -> 供外部使用,向promise上綁定待運行函數
jQuery.ready.promise()  -> 生成單例promise,綁定事件觸發completed()
complete()  -> 解綁觸發函數`detach()` + 無需等待時resolve那個promise`jQuery.ready()`

[源代碼]

// #3536
// readyList.promise() === jQuery.ready.promise()
var readyList;

jQuery.fn.ready = function( fn ) {

    // promise後加入回調
    jQuery.ready.promise().done( fn );
    return this;    // 鏈式
};

jQuery.extend( {

    // doScroll需借此推斷防止反復觸發
    isReady: false,

    // 須要幾次jQuery.ready()調用。才會觸發promise和自己定義ready事件
    readyWait: 1,

    holdReady: function( hold ) {
        if ( hold ) {
            // true,延遲次數 +1
            jQuery.readyWait++;
        } else {
            // 無參數。消減次數 -1
            jQuery.ready( true );
        }
    },

    // 觸發promise和自己定義ready事件
    ready: function( wait ) {

        // ready(true)時,消減次數的地方。也能取代幹ready()的事
        if ( wait === true ?

--jQuery.readyWait : jQuery.isReady ) { return; } // ready()調用時,標記dom已載入完畢 jQuery.isReady = true; // ready()能夠設置isReady,僅僅能消減默認的那1次 if ( wait !== true && --jQuery.readyWait > 0 ) { return; } // 觸發promise,jQuery.fn.ready(fn)綁定函數都被觸發 readyList.resolveWith( document, [ jQuery ] ); // 觸發自己定義ready事件,並刪除事件綁定 if ( jQuery.fn.triggerHandler ) { jQuery( document ).triggerHandler( "ready" ); jQuery( document ).off( "ready" ); } } } ); // 解綁函數 function detach() { if ( document.addEventListener ) { document.removeEventListener( "DOMContentLoaded", completed ); window.removeEventListener( "load", completed ); } else { document.detachEvent( "onreadystatechange", completed ); window.detachEvent( "onload", completed ); } } // detach() + jQuery.ready() function completed() { // readyState === "complete" is good enough for us to call the dom ready in oldIE if ( document.addEventListener || window.event.type === "load" || document.readyState === "complete" ) { detach(); jQuery.ready(); } } jQuery.ready.promise = function( obj ) { if ( !readyList ) { readyList = jQuery.Deferred(); // 推斷運行到這時。是否已經載入完畢 if ( document.readyState === "complete" ) { // 不再須要綁定不論什麽監聽函數。直接觸發jQuery.ready。延遲一會,等代碼運行完 window.setTimeout( jQuery.ready ); // Standards-based browsers support DOMContentLoaded } else if ( document.addEventListener ) { // Use the handy event callback document.addEventListener( "DOMContentLoaded", completed ); // 個別瀏覽器情況,錯過了事件仍可觸發 window.addEventListener( "load", completed ); // IE6-8不支持"DOMContentLoaded" } else { // Ensure firing before onload, maybe late but safe also for iframes document.attachEvent( "onreadystatechange", completed ); // A fallback to window.onload, that will always work window.attachEvent( "onload", completed ); // If IE and not a frame // continually check to see if the document is ready var top = false; try { top = window.frameElement == null && document.documentElement; } catch ( e ) {} if ( top && top.doScroll ) { ( function doScrollCheck() { // 防止反復觸發 if ( !jQuery.isReady ) { try { top.doScroll( "left" ); } catch ( e ) { return window.setTimeout( doScrollCheck, 50 ); } detach(); jQuery.ready(); } } )(); } } } return readyList.promise( obj ); }; // 運行。生成deferred對象。綁定好監聽邏輯 jQuery.ready.promise();

queue

jQuery提供了一個多用途隊列,animate加入的動畫就是使用默認的’fx’隊列完畢的。動畫的特點。是在元素上一經加入,即刻觸發。而且該元素一個動畫運行完,才會運行下一個被加入的動畫。動畫的運行是含有異步過程的,從這點上看,queue的價值是同意一系列函數被異步地調用而不會堵塞程序。jq隊列的實現,並非為了僅僅給動畫使用。由核心功能jQuery.queue/dequeue和外觀jQuery.fn.queue/dequeue/clearQueue/promise組成。

queue模型

以下是一個簡單的隊列模型。怎樣實現異步調用呢?在棧出的函數fn中傳入next參數就可以實現,僅僅要函數內調用next(),就可以實現異步調用下一個。

// 入隊
function queue( obj, fn ) {
    if ( !obj.cache ) obj.cache = [];
    obj.cache.push(fn);
}

// 出隊
function dequeue(obj) {
    var next = function() {
        dequeue(obj);
    }
    var fn = obj.cache.shift();
    if ( fn ) fn(next);
}

jquery實現

jquery的實現更精密,還考慮了隊列棧出為空後調用鉤子函數銷毀,type參數省略自己主動調整。功能自然是兩套:jQuery.xx/jQuery.fn.xx,使得$()包裹元素能夠叠代調用,而且$()調用時type為’fx’時。還將能夠加入時即刻運行。儲存位置都在私有緩存jQuery._data( elem, type )中。

API詳細功能見以下:

內部使用:(type不存在,則為’fx’,後參數不會前挪)
jQuery.queue( elem, type[, fn] ):向隊列加入fn,若fn為數組,則重定義隊列。type默認’fx’。這裏不會加入_queueHooks
jQuery.dequeue( elem, type):type默認’fx’,棧出隊列開頭並運行。若是為’fx’隊列。一旦被dequeue過。總是給隊列開頭添加有一個”inprogress”,之所以這麽做是為了滿足’fx’動畫隊列首個加入的函數要馬上運行。須要一個標記

還會添加jQuery._queueHooks鉤子,dequeue在隊列無函數時調用,會調用鉤子來刪除隊列對象和鉤子本身(極客精神-_-||)

外部使用:(type不為字符串,則為’fx’,且後參數會前挪)
jQuery.fn.queue( type, fn ):type默認’fx’,對於’fx’隊列,加入第一個fn時默認直接運行(動畫加入即運行的原因,第一個加入的開頭沒有”inprogress”)。其它則無此步驟。此方式加入fn都會給元素們的緩存加上用於自毀的鉤子jQuery._queueHooks( this, type )
jQuery.fn.dequeue( type ):對每一個元素遍歷使用jQuery.dequeue( this, type )
jQuery.fn.clearQueue( type ):重置隊列為空數組,type默認’fx’,不正確已綁定的_queuehook產生影響
jQuery.fn.promise( type, obj ): 返回一個deferred對象的promise對象,帶有jQuery._queueHooks鉤子的所有元素鉤子均被觸發時,觸發resolve(比方幾個元素動畫全都運行完後運行某操作)

在隊列中函數運行時。會向函數註入elem、next、hooks。通過next能夠讓函數內部調用jQuery.dequeue,hooks能夠讓函數內部調用empty方法直接終止、銷毀隊列。或者綁定銷毀時要運行的邏輯。

[源代碼]

// #4111。建議:內部使用接口
jQuery.extend( {
    // 有data為設置,無data為讀取,都返回該隊列
    queue: function( elem, type, data ) {
        var queue;

        if ( elem ) {
            type = ( type || "fx" ) + "queue";
            queue = jQuery._data( elem, type );

            // Speed up dequeue by getting out quickly if this is just a lookup
            if ( data ) {
                // data為數組,則直接替換掉原緩存值。原本無值,則指定為空數組
                if ( !queue || jQuery.isArray( data ) ) {
                    queue = jQuery._data( elem, type, jQuery.makeArray( data ) );
                } else {
                    // 將函數推入隊列
                    queue.push( data );
                }
            }
            return queue || [];
        }
    },

    dequeue: function( elem, type ) {
        type = type || "fx";

        var queue = jQuery.queue( elem, type ),
            startLength = queue.length,
            fn = queue.shift(),
            // 單例加入自毀鉤子empty方法,並取出
            hooks = jQuery._queueHooks( elem, type ),
            next = function() {
                jQuery.dequeue( elem, type );
            };

        /* 1、棧出、運行 */
        // 僅僅適用於‘fx‘隊列。凡被dequeue過,開頭都是"inprogress"。須要再shift()一次
        if ( fn === "inprogress" ) {
            fn = queue.shift();
            startLength--;
        }

        if ( fn ) {

            // ‘fx‘隊列,開頭加"inprogress"。用於表明隊列在運行中。不能馬上運行加入的函數
            if ( type === "fx" ) {
                queue.unshift( "inprogress" );
            }

            // 動畫中用到的,先無論
            delete hooks.stop;
            // 參數註入,可用來在fn內部遞歸dequeue
            fn.call( elem, next, hooks );
        }

        /* 2、銷毀 */
        // fn不存在,調用鉤子銷毀隊列和鉤子本身
        if ( !startLength && hooks ) {
            hooks.empty.fire();
        }
    },

    // 自毀鉤子,隊列無函數時dequeue會觸發。

存在元素私有緩存上 _queueHooks: function( elem, type ) { var key = type + "queueHooks"; return jQuery._data( elem, key ) || jQuery._data( elem, key, { empty: jQuery.Callbacks( "once memory" ).add( function() { // 銷毀隊列緩存 jQuery._removeData( elem, type + "queue" ); // 銷毀鉤子本身 jQuery._removeData( elem, key ); } ) } ); } } ); // #4179,用於外部使用的接口 jQuery.fn.extend( { queue: function( type, data ) { var setter = 2; /* 1、修正 */ // type默認值為‘fx‘ if ( typeof type !== "string" ) { data = type; type = "fx"; setter--; } /* 2、讀取 */ // 無data表示取值。僅僅取this[ 0 ]相應值 if ( arguments.length < setter ) { return jQuery.queue( this[ 0 ], type ); } /* 3、寫入 */ return data === undefined ? // 無data,返回調用者 this : this.each( function() { var queue = jQuery.queue( this, type, data ); // 此方法加入,一定會有hooks jQuery._queueHooks( this, type ); // ‘fx‘動畫隊列。首次加入函數直接觸發 if ( type === "fx" && queue[ 0 ] !== "inprogress" ) { jQuery.dequeue( this, type ); } } ); }, dequeue: function( type ) { // 遍歷觸發,以支持$(elems).dequeue(type) return this.each( function() { jQuery.dequeue( this, type ); } ); }, // 重置隊列為空(‘fx‘隊列也沒有了"inprogress",加入即觸發) clearQueue: function( type ) { return this.queue( type || "fx", [] ); }, // 返回promise。

調用者元素們所有緩存中的_queueHooks自毀均觸發,才會resolve這個promise promise: function( type, obj ) { var tmp, // 計數。hooks會添加計數值。

默認一次。在return前resolve()就會觸發這次。

count = 1, defer = jQuery.Deferred(), elements = this, i = this.length, // 消減計數,推斷promise是否觸發 resolve = function() { if ( !( --count ) ) { defer.resolveWith( elements, [ elements ] ); } }; // 修正type、data if ( typeof type !== "string" ) { obj = type; type = undefined; } type = type || "fx"; while ( i-- ) { // 凡是elem的type相應緩存中帶有hook鉤子的,都會添加一次計數 tmp = jQuery._data( elements[ i ], type + "queueHooks" ); if ( tmp && tmp.empty ) { count++; // 該隊列銷毀時會消減添加的這次計數 tmp.empty.add( resolve ); } } resolve(); return defer.promise( obj ); } } );

jQuery源代碼解析(3)—— ready載入、queue隊列