zoukankan      html  css  js  c++  java
  • 【jQuery源码】DOM Ready

    一直以来,各种JS最佳实践都会告诉我们,将JS放在HTML的最后,即</body>之前,理由就是:JS会阻塞下载,而且,在JS中很有可能有对DOM的操作,放在HTML的最后,可以尽可能的保证JS的执行在DOM加载完成之后。而如果放在onload事件中执行,如果页面有很多图像,那么页面的onload事件要过很久才会触发,因此DOM Ready事件就是最好的执行JS的时间了。

    所以,如果有个DOM Ready事件就好了,虽然现代浏览器已经支持DOMContentLoaded事件,但是我们还是得处理那些老旧的浏览器,于是DOM Ready事件就成了各个JS框架的必备功能啦。

    先来看看jQuery DOM Ready事件的用法吧,jQuery的DOM Ready事件用法很简单,大家都用过,下面的三种语法都是可用的:

    • $( document ).ready( handler )
    • $().ready( handler )(不推荐)
    • $( handler )

    还有一种方式:$(document).on('ready', handler);,已经在1.8版本里被废弃了,但是你还可以这么用。这种方式和ready方法的作用一样,但是如果ready事件已经被触发过了,这种方式绑定的handler将不会被执行,而且用这种方式绑定的handler会在以上三种方法绑定的handler被执行后再执行。

    一、DOM Ready检测

    下面,我们按照jQuery的思路,一步步的看一下jQuery如何处理DOM Ready事件的。

    1. 标准浏览器

    对于标准浏览器,我们可以添加DOMContentLoaded事件监听器:

    if ( document.addEventListener ) {
        document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
    }
    2. 非标准浏览器

    而对于非标准浏览器,我们可以监听document.onreadystatechange事件,如果document.readyState == ‘complete’时,可以认为DOM结构已经加载完成。

    if ( document.addEventListener ) {
        document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
    
        // If IE event model is used
    } else {
        document.attachEvent( "onreadystatechange", DOMContentLoaded );
    }

    在DOMContentLoaded函数中,若条件符合,会执行jQuery.ready()方法,同时移除事件监听。

    DOMContentLoaded = function() {
        if ( document.addEventListener ) {
            document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false );
            jQuery.ready();
        } else if ( document.readyState === "complete" ) {
            // we're here because readyState === "complete" in oldIE
            // which is good enough for us to call the dom ready!
            document.detachEvent( "onreadystatechange", DOMContentLoaded );
            jQuery.ready();
        }
    };
    3. 安全起见

    上面的检测方式应该够了,但是处于安全的考虑,万一前面的判断失效了怎么办。这时候,onload事件虽然可能触发的比较晚,但是它能可靠的被触发,因此在判断中添加了对onload事件的监听:

    if ( document.addEventListener ) {
        document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
    
        // A fallback to window.onload, that will always work
        window.addEventListener( "load", jQuery.ready, false );
    
        // If IE event model is used
    } else {
        document.attachEvent( "onreadystatechange", DOMContentLoaded );
    
        // A fallback to window.onload, that will always work
        window.attachEvent( "onload", jQuery.ready );
    }

    这里不用担心jQuery.ready()会被多次触发,因为在jQuery.ready()中有检测是否被触发过了。

    4. 还是IE

    在IE中,onreadystatechange事件对于iframe的来说可能会有延迟,但是却足够安全。但这个事件对于非iframe有时会不太可靠,特别是在页面中图片资源比较多的时候,可能反而在onload事件触发之后才能触发,因此对于这种情况,需要做进一步的检测:

    if ( document.addEventListener ) {
        document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
    
        // A fallback to window.onload, that will always work
        window.addEventListener( "load", jQuery.ready, false );
    
        // If IE event model is used
    } else {
        document.attachEvent( "onreadystatechange", DOMContentLoaded );
    
        // A fallback to window.onload, that will always work
        window.attachEvent( "onload", jQuery.ready );
    
        // 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) {}
    
        // 如果是非iframe的情况,并且支持doScroll方法
        if ( top && top.doScroll ) {
            // 在这个自执行函数中,调用top.doScroll方法,若报错,则延时50ms再检测;若不报错,则表示DOM Ready,可以执行jQuery.ready()方法了
            (function doScrollCheck() {
                if ( !jQuery.isReady ) {
                    try {
                        top.doScroll("left");
                    } catch(e) {
                        return setTimeout( doScrollCheck, 50 );
                    }
                    // and execute any waiting functions
                    jQuery.ready();
                }
            })();
        }
    }

    是的,JS就是这么一门神(dan)奇(teng)的语言,每个浏览器的实现可能不尽相同,所以就出现了这种对一个事件的监听要做如此多的兼容考虑的情况。

    5. Ready太晚了

    还有一种情况是,当我们调用$(document).ready()的时候,DOM结构已经加载完成了,这时候可以直接调用jQuery.ready,但是因为IE的问题,需要延迟调用,所以有下面的代码,至于为什么要这么用,我也没看明白:

    if ( document.readyState === "complete" ) {
        // Handle it asynchronously to allow scripts the opportunity to delay ready
        setTimeout( jQuery.ready, 1 );
    } else if ( document.addEventListener ) {
        document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
    
        // A fallback to window.onload, that will always work
        window.addEventListener( "load", jQuery.ready, false );
    
        // If IE event model is used
    } else {
        document.attachEvent( "onreadystatechange", DOMContentLoaded );
    
        // A fallback to window.onload, that will always work
        window.attachEvent( "onload", jQuery.ready );
    
        // 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) {}
    
        // 如果是非iframe的情况,并且支持doScroll方法
        if ( top && top.doScroll ) {
            // 在这个自执行函数中,调用top.doScroll方法,若报错,则延时50ms再检测;若不报错,则表示DOM Ready,可以执行jQuery.ready()方法了
            (function doScrollCheck() {
                if ( !jQuery.isReady ) {
                    try {
                        top.doScroll("left");
                    } catch(e) {
                        return setTimeout( doScrollCheck, 50 );
                    }
                    // and execute any waiting functions
                    jQuery.ready();
                }
            })();
        }
    }

    到这里,jQuery对DOM Ready的检测就完成了,那么$(document).ready()是怎么工作的呢?

    二、DOM Ready全过程

    这里,我们按代码执行的顺序,来理解jQuery的DOM Ready的全部代码,看jQuery是如何一步步处理DOM Ready事件的。为了阅读的方便,我将代码做了删减,并对代码顺序做了调整,大家可以按从1到7的顺序看代码。这里不对Deferred对象做解释,因为我也还没有看,只是记住他的作用是维护一系列条件触发的回调函数。

    jQuery.fn = jQuery.prototype = {
        init: function( selector, context, rootjQuery ) {
            if ( jQuery.isFunction( selector ) ) {
                // 当调用$(callback); 时,会调用$(document).ready(callback);      Ⅰ
                return rootjQuery.ready( selector ); 
            }
        },
    
        ready: function( fn ) {
            // Add the callback
            // 调用jQuery.ready.promise方法,返回一个Deferred对象 readyList,
            // 然后将fn加入到readyList的成功的回调列表中。
            // 如果readyList已存在,则直接返回readyList,然后直接调用这个回调函数fn
            jQuery.ready.promise()                               Ⅱ
                .done( fn );                              Ⅳ
    
            return this;
        }
    };
    
    jQuery.ready.promise = function( obj ) {
        if ( !readyList ) {
    
            readyList = jQuery.Deferred();
    
            // IE的bug
            if ( document.readyState === "complete" ) {
                setTimeout( jQuery.ready, 1 );
    
                // 给标准浏览器绑定DOMContentLoaded事件以触发DOMContentLoaded方法
                // 同时,绑定window.onload事件作为备用方案
            } else if ( document.addEventListener ) {
                document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
                window.addEventListener( "load", jQuery.ready, false );
    
            // 给IE浏览器绑定onreadystatechange事件,在DOMContentLoaded中判断
            // readyState是否为complete
            } else {
                document.attachEvent( "onreadystatechange", DOMContentLoaded );
                window.attachEvent( "onload", jQuery.ready );
    
                // 对IE做进一步的兼容性检测
                var top = false;
                try {
                    top = window.frameElement == null && document.documentElement;
                } catch(e) {}
                if ( top && top.doScroll ) {
                    (function doScrollCheck() {
                        if ( !jQuery.isReady ) {
                            try {
                                // Use the trick by Diego Perini
                                // http://javascript.nwbox.com/IEContentLoaded/
                                top.doScroll("left");
                            } catch(e) {
                                return setTimeout( doScrollCheck, 50 );
                            }
                            // and execute any waiting functions
                            jQuery.ready();
                        }
                    })();
                }
            }
        }
        // 返回readyList 
        return readyList.promise( obj );                 Ⅲ
    };
    
    // 接下来就是等待DOMContentLoaded/readyState==='complete'事件的发生了
    // 移除DOMReady事件监听,执行jQuery.ready()
        DOMContentLoaded = function() {                  Ⅴ
            // 标准浏览器
            if ( document.addEventListener ) {
                document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false );
                jQuery.ready();
            // IE 浏览器
            } else if ( document.readyState === "complete" ) {
                document.detachEvent( "onreadystatechange", DOMContentLoaded );
                jQuery.ready();
            }
        };
    
    jQuery.extend({
        // DOM Ready标志位,DOM Ready触发后置为true
        isReady: false,
    
        // DOM Ready后运行
        ready: function( ) {
    
            // 如果已经Ready,则直接返回
            if ( jQuery.isReady ) {
                return;
            }
    
            // 兼容IE
            if ( !document.body ) {
                return setTimeout( jQuery.ready, 1 );
            }
    
            // 将isReady置为true
            jQuery.isReady = true;              Ⅵ
    
            // 这里是真正触发我们传入的回调的地方!!!! 
            // 传入上下文和回调函数的参数列表
            // 注意啦,上下文是document,也就是说我们在`$(document).ready(fn);`里的fn中的this是`document`,不是`window`
            // 参数列表传入了jQuery,意味着即使我们调用了`$.noConflict()`将$让渡出去了,我们仍然能在fn中定义第一个参数为$,代表jQuery
            readyList.resolveWith( document, [ jQuery ] );              Ⅶ
    
            // 这段代码是为了触发这种方式添加的DOM Ready回调: $(document).on('ready' , fn);
            // 这种方式在1.8被废弃,但是没有被移除
            // 你可以看到这种方式添加的DOM Ready回调是最后被执行的
            if ( jQuery.fn.trigger ) {
                jQuery( document ).trigger("ready").off("ready");
            }
        }
    });

    看完代码,让我们再来看一下jQuery的思路:

    • Ⅰ. 调用rootjQuery的ready事件
    • Ⅱ. 调用jQuery.ready.promose(),若readyList为空,将readyList赋值为一个Deferred对象;否则将回调函数添加到readyList成功回调列表中,然后跳转到第7步
    • Ⅲ. 在jQuery.promise()中为DOM Ready事件添加监听器
    • Ⅳ. 将回调函数fn添加到readyList的成功回调列表中
    • Ⅴ. 当DOM Ready事件发生时,移除监听器,执行jQuery.ready()
    • Ⅵ. 在jQuery.ready()中将isReady置为true
    • Ⅶ. 执行readyList的成功回调列表中的方法
  • 相关阅读:
    重写保存按钮save事件
    隐藏列获取不到值,表格选中行提示未选中
    前后台获取上下文context
    editGrid分录表格
    通用查询-高级查询
    js保留位和取整
    在Visual Studio中使用C++创建和使用DLL
    Lua中的一些库(1)
    Lua中的面向对象编程
    Lua中的模块与包
  • 原文地址:https://www.cnblogs.com/shytong/p/5376624.html
Copyright © 2011-2022 走看看