jQuery源碼分析-10事件處理-Event-DOM-ready

 

作者:nuysoft/JS攻城師/高雲 QQ:47214707 EMail:nuysoft@gmail.com    

 

後文預告:封裝事件對象 便捷接口解析  

 

前記:  

JavaScript同其他語言一樣,基礎非常重要,僅僅會用框架和工具寫一些WebApp是不能成為一名合格的JS工程師的,建議多讀幾次JavaScript權威指南,以我的經驗與Java相比,JavaScript需要花費與Java相當的時間才能達到與Java相當的水平。  

 

jQuery完結之後計劃寫Java開源框架的源碼解析,從Spring、Hibernate開始,有興趣的同學可以一起研究。

JavaScript同其他語言一樣,基礎非常重要,僅僅會用框架和工具寫一些WebApp是不能成為一名合格的JS工程師的,建議多讀幾次JavaScript權威指南,以我的經驗與Java相比,JavaScript需要花費與Java相當的時間才能達到與Java相當的水平。

 

jQuery完結之後計劃寫Java開源框架的源碼解析,從Spring、Hibernate開始,有興趣的同學可以一起研究。Js代碼 

10.8    DOM ready  

 

初學jQuery時,很可能學到的第一個知識點就是.ready(),但是網絡上流轉的文章和API對此的解釋或剖析,經常是模棱兩可甚至是不準確或錯誤的,本節將詳細的闡述.ready()的用法和原理,並糾正這些誤區。  

 

10.8.1  如何使用.ready()  

 

.ready() 指定一個事件句柄,當DOM完全加載完成時執行  

 

雖然JavaScript提供瞭load事件,當頁面渲染完成之後會執行這個函數,在所以元素加載完成之前,這個函數不會被調用,例如圖像。但是在大多數情況下,隻要DOM結構加載完,腳本就可以盡快運行。傳遞給.ready()的事件句柄在DOM準備好後立即執行,因此通常情況下,最好把綁定事件句柄和其他jQuery代碼都到這裡來。但是當腳本依賴於CSS樣式屬性時,一定要在腳本之前引入外部樣式或內嵌樣式的元素。  

 

如果代碼依賴於需加載完的元素(例如,想獲取一個圖片的尺寸大小),應該用.load()事件代替,並把代碼放到load事件句柄中。  

 

.ready()方法不同於<body onload="">屬性。如果必須用load事件,那麼就不要使用.ready(),或用.load()代替,把load事件句柄綁定到window或其他的具體元素上,例如圖片。  

 

以下三個語法全部是等價的:  

$(document).ready(handler)  

$().ready(handler) (this is not recommended)  

$(handler)  

 

$(document).bind("ready", handler)的行為類似於ready方法,但有一個例外:如果ready事件已觸發後,再嘗試用.bind("ready")綁定的處理函數將不會被執行。而用以上三種方法綁定的句柄會被執行,無論是什麼時候綁定的。  

.ready()方法隻能在包含瞭document的jQuery上調用,因此選擇器可以省略。  

.ready()方法典型的用法是使用一個匿名函數:  

$(document).ready(function() {  

  // Handler for .ready() called.  

});  

等價於:  

$(function() {  

 // Handler for .ready() called.  

});  

 

如果在DOM加載完畢之後調用.ready(),新的句柄將會立即執行。  

 

特別強調兩點:  

1.  之所以花費篇幅來闡述.ready()如何使用(翻譯的官網文檔),是因為網絡上的一些文章和中文API不準確甚至是錯誤的(應該是工具翻譯的),所以需謹慎參考  

2.  .ready()的觸發要早於.load(),但是不要完全迷信依賴,如果要獲取或操作樣式,或依賴於像圖片這樣的必須加載完成才能獲取和操作的元素,就用<body onload="">或.load()  

 

10.8.2  源碼分析  

 

到底.ready()是如何實現更快的觸發ready事件的呢,關鍵在jQuery.bindReady方法的實現裡:  

 

對標準瀏覽器綁定:document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );  

對IE瀏覽器綁定:document.attachEvent( "onreadystatechange", DOMContentLoaded );  

 

當然還有一些其他的關鍵技巧,現在完成的看下jQuery.bindReady的實現:  

 

// 綁定DOM ready監聽器,跨瀏覽器,兼容標準瀏覽器和IE瀏覽器  

bindReady: function() { // jQuery.bindReady  

if ( readyList ) {  

    return;  

}  

 

readyList = jQuery._Deferred(); // 初始化ready異步事件句柄隊列  

 

// Catch cases where $(document).ready() is called after the  

// browser event has already occurred.  

// 如果DOM已經完畢,立即調用jQuery.ready  

if ( document.readyState === "complete" ) {  

    // Handle it asynchronously to allow scripts the opportunity to delay ready  

    // 重要的是異步  

    return setTimeout( jQuery.ready, 1 );  

}  

 

// Mozilla, Opera and webkit nightlies currently support this event  

// DOM 2級事件模型,Mozilla, Opera, webkit等   

if ( document.addEventListener ) {  

    // Use the handy event callback  

    // 使用快速事件句柄  

    document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );  

 

    // A fallback to window.onload, that will always work  

    // 再在window上綁定load事件句柄,這個句柄總是會執行  

    // 為什麼同時綁定document window呢?我想是為瞭安全起見,防禦性編碼!  

    window.addEventListener( "load", jQuery.ready, false );   

 

// If IE event model is used  

} else if ( document.attachEvent ) {  

    // ensure firing before onload,  

    // maybe late but safe also for iframes  

    // 同樣onreadystatechange會在onload之前觸發,但是對於iframe會有延遲但安全一定會觸發  

    // 看看DOMContentLoaded的實現,是檢測document.readyState的狀態是否為complete,這裡有些像ajax中的檢測   

    document.attachEvent( "onreadystatechange", DOMContentLoaded );  

 

    // A fallback to window.onload, that will always work  

    window.attachEvent( "onload", jQuery.ready ); // 同樣的安全起見,防禦性編碼!及時前邊的所以hack技巧失敗瞭,onload是最後的保障  

 

    // If IE and not a frame  

    // continually check to see if the document is ready  

    var toplevel = false;  

 

    try {  

        toplevel = window.frameElement == null; // 檢查window的frameElement屬性,看是否是頂層窗口  

    } catch(e) {}  

      

    // 做doScroll檢測,如果在iframe中就不檢測瞭,onreadystatechange對於ifame很可靠  

    if ( document.documentElement.doScroll && toplevel ) {  

        doScrollCheck();   

        // 在doScrollCheck中,不斷的(每隔1ms)執行document.documentElement.doScroll("left"),直到不拋出異常為止  

        // 這是IE下檢測DOM ready的技巧  

        }  

    }  

},  

 

然後是完整的ready相關方法的分析,我剔除瞭其他無關的代碼,保留瞭整體結構和相關方法:  

 

(function( window, undefined ) {  

 

    var jQuery = (function() {  

          

        // Define a local copy of jQuery  

        var jQuery = function( selector, context ) {  

                // The jQuery object is actually just the init constructor 'enhanced'  

                var ret = new jQuery.fn.init( selector, context, rootjQuery );  

                return ret;  

            },  

          

            // A central reference to the root jQuery(document)  

            rootjQuery, // 包含瞭document的jQuery對象  

          

            // The deferred used on DOM ready  

            readyList, // ready事件處理函數隊列  

          

            // The ready event handler  

            DOMContentLoaded, // DOM ready hack句柄,.ready()能早於.load()的關鍵所在  

          

        jQuery.fn = jQuery.prototype = {  

            constructor: jQuery,  

            init: function( selector, context, rootjQuery ) {  

                // HANDLE: $(function)  

                // Shortcut for document ready  

                // 如果函數,則認為是DOM ready句柄  

                if ( jQuery.isFunction( selector ) ) {  

                    return rootjQuery.ready( selector );  

                }  

            },  

          

            ready: function( fn ) {  

                // Attach the listeners  

                jQuery.bindReady(); // 綁定DOM ready監聽器,跨瀏覽器,兼容標準瀏覽器和IE瀏覽器  

          

                // Add the callback  

                readyList.done( fn ); // 將ready句柄添加到ready異步句柄隊列  

          

                return this;  

            }  

        };  

          

        // Give the init function the jQuery prototype for later instantiation  

        jQuery.fn.init.prototype = jQuery.fn;  

          

        jQuery.extend = jQuery.fn.extend = function() {};  

          

        jQuery.extend({  

          

            // Is the DOM ready to be used? Set to true once it occurs.  

            // DOM是否加載完畢  

            isReady: false,  

          

            // A counter to track how many items to wait for before  

            // the ready event fires. See #6781  

            // DOM加載完畢之前的等待次數  

            readyWait: 1,  

          

            // Hold (or release) the ready event  

            // 是否延遲觸發DOM ready?在jQuery中沒有任何地方有調用到holdReady,這是個遺留方法還是預留方法,有待繼續研究。  

            // 因此在當前版本中,readyWait總是1,直到自減  

            holdReady: function( hold ) {  

                if ( hold ) { // 繼續等待  

                    jQuery.readyWait++;  

                } else {  

                    jQuery.ready( true ); //   

                }  

            },  

          

            // Handle when the DOM is ready  

            // 判斷DOM是否加載完畢,如果已完畢,調用DOM ready事件異步隊列readyList,如果未完,每個1ms檢查一次  

            ready: function( wait ) { // jQuery.ready  

                // Either a released hold or an DOMready/load event and not yet ready  

                // 條件1:wait === true && !–jQuery.readyWait 還在等待加載完成,但是等待計數器jQuery.readyWait已經是0,  

                // 換句話說參數wait為true表示嘗試一下看是否能開始調用readyList瞭,如果發現計數器jQuery.readyWait變成0,啥也不管瞭,開始調用吧  

                // 再換句話說,計數器已經是0瞭,開幹吧  

                  

                // 條件2:wait !== true && !jQuery.isReady 明確的說不用瞭,即使DOM ready標記還是false  

                // 換句話說,及時DOM ready標記還是false,但是調用jQuery.ready的客戶端認為不比再等瞭,可以開幹瞭  

                if ( (wait === true && !–jQuery.readyWait) || (wait !== true && !jQuery.isReady) ) {  

                    // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).  

                    // 檢查document.body是否存在,這是特定於IE的檢測,保證在IE中DOM ready事件能正確判斷  

                    if ( !document.body ) {  

                        return setTimeout( jQuery.ready, 1 );  

                    }  

 

                    // Remember that the DOM is ready  

                    // 到這裡,jQuery.isReady被強制為true  

                    // 在條件2中,如果jQuery.isReady為true,那麼說明readyList的狀態已經確定,添加到readyList中的函數會被立即執行  

                    // 如果jQuery.isReady為是false,那麼接下來readyList中函數將被執行  

                    jQuery.isReady = true;  

 

                    // If a normal DOM Ready event fired, decrement, and wait if need be  

                    // 雖然上邊的條件2的意思是,別管其他情況,調用方認為ready瞭,但是這裡仍然做瞭防禦性檢測,如果等待計數器仍然大於1,結束ready調用  

                    // 如果這個條件成立,那麼一定是哪裡出問題瞭!  

                    // 在當前版本1.6.1中,這個判斷永遠不會成立,因為沒有地方調用會使readyWait加一的holdReady  

                    if ( wait !== true && –jQuery.readyWait > 0 ) {  

                        return;  

                    }  

 

                    // If there are functions bound, to execute  

                    // 調用DOM ready事件異步隊列readyList,註意整理的參數亮瞭!  

                    // document指定瞭ready句柄的上下文,這樣我們在執行ready事件句柄時this指向document  

                    // [ jQuery ]指定瞭ready事件句柄的第一個參數,這樣即使調用$.noConflict()交出瞭$的控制權,我們依然可以將句柄的第一個參數命名為$,繼續在句柄內部使用$符號  

                    // 如此的精致!贊嘆之餘,但不要在你的項目中也這麼用,理解和維護將成為最大的難題。  

                    readyList.resolveWith( document, [ jQuery ] );  

 

                    // Trigger any bound ready events  

                    if ( jQuery.fn.trigger ) {  

                        // 觸發ready事件,然後刪除ready句柄,DOM ready事件隻會生效一次,ready是個自定義事件  

                        jQuery( document ).trigger( "ready" ).unbind( "ready" );  

                    }  

                }  

            },  

          

            // 綁定DOM ready監聽器,跨瀏覽器,兼容標準瀏覽器和IE瀏覽器  

            bindReady: function() { // jQuery.bindReady  

                if ( readyList ) {  

                    return;  

                }  

 

                readyList = jQuery._Deferred(); // 初始化ready異步事件句柄隊列  

 

                // Catch cases where $(document).ready() is called after the  

                // browser event has already occurred.  

                // 如果DOM已經完畢,立即調用jQuery.ready   

                if ( document.readyState === "complete" ) {  

                    // Handle it asynchronously to allow scripts the opportunity to delay ready  

                    // 重要的是異步  

                    return setTimeout( jQuery.ready, 1 );  

                }  

 

                // Mozilla, Opera and webkit nightlies currently support this event  

                // DOM 2級事件模型,Mozilla, Opera, webkit等   

                if ( document.addEventListener ) {  

                    // Use the handy event callback  

                    // 使用快速事件句柄  

                    document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );  

 

                    // A fallback to window.onload, that will always work  

                    // 再在window上綁定load事件句柄,這個句柄總是會執行  

                    // 為什麼同時綁定document window呢?我想是為瞭安全起見,防禦性編碼!  

                    window.addEventListener( "load", jQuery.ready, false );   

 

                // If IE event model is used  

                } else if ( document.attachEvent ) {  

                    // ensure firing before onload,  

                    // maybe late but safe also for iframes  

                    // 同樣onreadystatechange會在onload之前觸發,但是對於iframe會有延遲但安全一定會觸發  

                    // 看看DOMContentLoaded的實現,是檢測document.readyState的狀態是否為complete,這裡有些像ajax中的檢測   

                    document.attachEvent( "onreadystatechange", DOMContentLoaded );  

 

                    // A fallback to window.onload, that will always work  

                    window.attachEvent( "onload", jQuery.ready ); // 同樣的安全起見,防禦性編碼!及時前邊的所以hack技巧失敗瞭,onload是最後的保障  

 

                    // If IE and not a frame  

                    // continually check to see if the document is ready  

                    var toplevel = false;  

 

                    try {  

                        toplevel = window.frameElement == null; // 檢查window的frameElement屬性,看是否是頂層窗口  

                    } catch(e) {}  

                      

                    // 做doScroll檢測,如果在iframe中就不檢測瞭,onreadystatechange對於ifame很可靠  

                    if ( document.documentElement.doScroll && toplevel ) {  

                        doScrollCheck();   

                        // 在doScrollCheck中,不斷的(每隔1ms)執行document.documentElement.doScroll("left"),直到不拋出異常為止  

                        // 這是IE下檢測DOM ready的技巧  

                    }  

                }  

            },  

        });  

          

        // All jQuery objects should point back to these  

        rootjQuery = jQuery(document);  

          

        // Cleanup functions for the document ready method  

        // 構造瀏覽器加載完畢事件處理函數DOMContentLoaded,需要檢測瀏覽器添加事件的方法  

        if ( document.addEventListener ) {  

            DOMContentLoaded = function() {  

                // document ready之後移除  

                document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false );  

                jQuery.ready();  

            };  

          

        } else if ( document.attachEvent ) {  

            DOMContentLoaded = function() {  

                // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).  

                // 確保body是存在的,IE會有一些延遲  

                if ( document.readyState === "complete" ) {  

                    // 先移除,難道瀏覽器重新渲染會再次觸發onreadystatechange嗎?  

                    document.detachEvent( "onreadystatechange", DOMContentLoaded );  

                    jQuery.ready();  

                }  

            };  

        }  

          

        // The DOM ready check for Internet Explorer  

        // 在IE裡,每隔1ms檢測IE瀏覽器的ready狀態  

        function doScrollCheck() {  

            if ( jQuery.isReady ) {  

                return;  

            }  

          

            try {  

                // If IE is used, use the trick by Diego Perini  

                // https://javascript.nwbox.com/IEContentLoaded/  

                // 如果是IE,則使用該技巧來檢測瀏覽器加載狀態  

                document.documentElement.doScroll("left");  

            } catch(e) {  

                setTimeout( doScrollCheck, 1 );  

                return;  

            }  

          

            // and execute any waiting functions  

            // 執行其他的等待函數  

            jQuery.ready();  

        }  

          

        // Expose jQuery to the global object  

        return jQuery;  

      

    })();  

 

    // 見系列文章中關於異步隊列的解析  

    jQuery.extend({  

        // Create a simple deferred (one callbacks list)  

        _Deferred: function() {},  

      

        // Full fledged deferred (two callbacks list)  

        Deferred: function( func ) {},  

      

        // Deferred helper  

        when: function( firstParam ) {}  

    });  

      

    // 見系列文章中關於事件的解析  

    jQuery.event = {  

        trigger: function( event, data, elem, onlyHandlers ) {},  

 

        handle: function( event ) {},  

 

        special: {  

            ready: {  

                // Make sure the ready event is setup  

                setup: jQuery.bindReady,  

                teardown: jQuery.noop  

            }  

        }  

    };  

 

    window.jQuery = window.$ = jQuery;  

})(window);   

發佈留言