2017年3月17日 星期五

jq_3


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

標籤: jqueryreadyqueue
 474人閱讀 評論(0) 收藏 舉報
 分類:
目錄(?)[+]
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;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
有了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()`
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3
[源碼]
// #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();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142

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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
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 );
    }
} );
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158