onload事件以及DOMReady事件兼容方案

曾经刚做前端时,再加上浏览器对js兼容性很不熟悉,所以很长一段时间都是用jquery,写一些简单的交互效果快速而绚丽。其中$(document).ready(function(){ ... });是在所有js代码前首先会写的,只知道这个方法是对onload事件的替代方法,当时在我心中就是将onload等于jquery的ready事件的,后来才知道它们是两个不同的事件,JQ的ready事件比onload相应的时间更早,在JQ的ready事件在DOM树渲染完成时就立即执行了,而原生JS中的onload事件是等到页面中的所有元素接收完数据后再去执行,还有一点就是在各个浏览器中还有一些不同之处。

window.onload 与 document.body.onload 事件及 BODY 标签内的内联 onload 事件三者的关系

window.onload 与 document.body.onload事件在各个浏览器中都是存在的,document.onload确是不存在的,虽然document.onload似乎是我们想要的。

在不同浏览器中window.onload 与 document.body.onload事件还存在一些差异性,实例代码如下:

<button onclick="alert(document.body.onload);">查看本页 document.body.onload 事件</button>
<button onclick="alert(window.onload);">查看本页 window.onload 事件</button>
<button onclick="alert(window.onload===document.body.onload);">查看document.body.onload 事件与 window.onload 事件是否为同一事件</button>
<script>
    document.body.onload = function (){ // 函数 A
        document.getElementById("A").innerHTML = "document.body.onload 被触发";
    };
    window.onload = function (){ // 函数 B
        document.getElementById("B").innerHTML = "window.onload 被触发";
    };
</script>
<span id="A" style="background:gold"></span>
<span id="B" style="background:gray"></span>
        
window.onload 与 document.body.onload事件

复制以上代码在各个浏览器内运行查看实际效果,将函数A和函数B调换查看函数覆盖效果。得到如下结论:

  • 在IE6、7、8中:即使在定义了 window.onload 事件函数,也会是 null ,但是window.onload 事件函数的事件处理函数始终为 document.body.onload 提供。
  • 在 Firefox 中:window.onload 事件与 document.body.onload 事件是不同的,所以后者事件函数永远不会覆盖前者。
  • 在Chrome/Safari/Opera/中:window.onload 事件与 document.body.onload 事件是相同的,所以后者事件函数会覆盖前者。

那么BODY 标签内的内联 onload 事件跟他们又是什么关系么?如下实例代码:

<script>
    window.onload=function(){
        alert("window onload");
    }
</script>
<body onload="alert('inline window onload')">
</body>
        
BODY标签内 onload 事件与 window.onload 事件

在所有浏览器中都是后者的事件函数,所以就能说明BODY标签内 onload 事件与 window.onload 事件是同一事件处理机制。

为什么很多javascript书籍里只见到window.onload没见到document.body.onload 事件呢?我猜大概是因为如果将document.body.onload提到head里,会出现document.body is null的提示错误,就像元素元素没找一样。

所以很多javascript书籍里的事例代码都是用window.onload不用document.body.onload 来说明实例,可能怕初学者刚学习就遇到这么个错误而影响javascript的学习~~~。在《JavaScript DOM编程艺术》有这样一段代码能让window.onload内的多个函数不被覆盖的方法:

function addload(func) {
    var old = window.onload;
    if (typeof window.onload != "function") {
        window.onload = func;
    } else {
        window.onload = function() {
            old();
            func();
        }
    }
};
// 添加运行函数
addload(aaa);
addload(bbb);
        
解决window.onload事件不被覆盖的经典方案

接下里不引入JQ我们自己写一个$(document).ready的方法。

DOMContentLoaded事件

使用DOMReady不而用window.onload事件的原因:当DOM树解析完成,再诸如图片、脚本、样式表甚至是iframe中所有资源接收完数据后后window.onload才会触发。如果页面中下载资源太多,window.onload触发程序函数要等到全部资源接收完数据后才会执行,如果用户点击选项卡之类的就会没反应,比较影响用户体验。但是在现代浏览器增加了DOMContentLoaded事件,它是在当DOM树解析完成就触发的,所以这就是我们想要的,但是在IE6/7/8没有此事件,所以接下来就是关于该事件的兼容新方案问题展开研究。

主流框架中DOMContentLoaded事件的实现这样转载文章中,就介绍了各大主流框架DOMContentLoaded事件的实现代码和思路,觉得不错就转载过来,我也就可以不啰嗦了。

看了上篇文章回来,我们知道解决各大Js框架中针对IE6/7/8的DOMContentLoaded兼容性方案,如下:

在readyState

  1. readyState:监听document的onreadystatechange事件,各大JS框架都是监听readyState”complete”或”loaded”,而不知此时虽然是在资源接收完数据之前,但是这已经在资源发送请求之后了。
  2. 加入script:document.write在页面加入defer属性的<script>(defer属性的<script>能延迟加载js),此方法是监听script的readyState状态,但是defer属性的<script>在IE浏览器中的实现方式有差异,特别是在页面还有iframe时。
  3. doScroll :微软文档指出doScroll在DOM结构稳定后能够访问。

标准的onreadystatechange应该有五种状态:”uninitalized”(未初始化)、”loading”(加载)、”interactive”(交互)、”complete” (完成)、”loaded”(加载完毕),document.readyState属性已被大部分浏览器支持,但是属性的在IE6/7/8中之间有细微差别。IE7/8能保证最后一个是“loaded”状态,但是IE6会因为脚本的载入顺序不同导致“loaded”和“complete”不一定是最后一个状态。所以我们通过doScroll判断DOM是否准备完毕。下面会说到为什么要使用setTimeout()来调用函数的理由

综上所诉及对现在浏览器的情况,我们对DOMReady代码采用以下几点思路:

  • 除了IE6/7/8就使用doScroll方法,对于各大JS框架现在还对低版本的wibkit采取hack,我觉得已经没必要了,wibkit不支持DOMContentLoaded 事件是07年的事了。
  • 在DOMContentLoaded事件触发之前调用push(fn),所有的fn将被push到一个数组队列中;当 DOMContentLoaded 事件触发时,按顺序执行。
  • 因为IE9以上支持addEventListener事件模型,所以就可以利用这个函数将IE6/7/8跟其他其他支持DOMContentLoaded事件事件的浏览器分开。
  • IE6/7/8利用定时器不断的去检查doScroll方法是否可以使用,为了代码的高效性,bind()只能执行一次,所以如果执行bind()就立即返回。
  • 因为javascript是单线程机制的语言,如果同一时间内有其他代码需要运行的话,则这些代码需要等到javascript引擎执行完当前代码之后才有执行机会,所以使用setTimeout()将这个耗性能的操作推迟执行,让出javascript引擎里处理其他队列中的事件,一旦等javascript引擎空闲了,就会尽快的执行,所谓的尽快是因为setTimeout()中的第二个参数是0,0不代表0ms后就执行了,这个取决于系统&&浏览器最快值(依稀记得苹果系统比windows系统这个最快值要小,浏览器的话应该IE是最慢的吧)。

所以最后的DOMReady程序如下:

var DOM = (function(){
    var readyBound = false;
    var _DOM = {
        fn: [],
        bReady : false,
        /**
         * 注册 DOM ready时的处理函数
         */
        push: function ( fn ){
            _DOM.bind();
            // 如果DOM已经准备好了 就直接调用注册的fn
            if ( _DOM.bReady ) {
                fn.call( document );
                // 否则 就将fn推入readyLis
            } else {
                _DOM.fn.push( fn );
            }
        },

        /**
         * 触发DOM ready的函数
         */
        ready: function (){
            var fn, i = 0, fns;
            _DOM.bReady = true;
            //为了减少变量查询带来的性能损耗,将fn赋值给本地变量
            fns = _DOM.fn;

            while( (fn = fns[ i++ ]) ){
                fn.call( document );
            }
            //清空
            fns = null;
        },

        /**
         * 监听 DOM ready事件
         */
        bind: function (){
            if (readyBound) return; //如果执行过bind()函数则返回
            readyBound = true;

            //如果在绑定ready的时候DOM已经准备好了,就异步调用ready函数
            if ( document.readyState === "complete" ) {
                return setTimeout( _DOM.ready,0);
            }

            if( document.addEventListener ){

                document.addEventListener( "DOMContentLoaded", function(){
                    document.removeEventListener( "DOMContentLoaded", arguments.callee, false );
                    _DOM.ready();
                }, false );


            } else {// else if( document.attachEvent )

                //使用doScroll方法来检测DOM树是否渲染完成

                var bTop = false;

                try {
                    //处于iframe中,使用doScroll方法并不代表DOM加载完毕
                    bTop = window.frameElement == null;
                } catch(e) {}


                //用bTop标识是否使用doScroll来触发ready函数
                if ( document.documentElement.doScroll && bTop ) {
                    (function(){
                        try{
                            document.documentElement.doScroll('left');
                        }catch(e){
                            return setTimeout( arguments.callee, 0 );
                        }
                        _DOM.ready();
                    })();
                }
            }
        }
    };
    return _DOM.push;
})();
        
强大而精简的DOM ready函数

这样子一个DOM ready就完成了,这个函数主体结构和代码不是我自己原创的,我只是拿来学习和分析,再加以修改,如果有错误,还望高手指点啊。

最后,我在测试时发现请求资源多的时候,后面的几个会有一段长时间的阻塞时期,后来网上搜到一篇文章说,浏览中的网络操作虽然是多个线程并行执行的,但是并行连接数是有限的,通常为 2 至 6 个。所以DOMReady虽然能让script尽可能异步执行,但是还是得保证页面中内联元素的网络请求数量。

本文源链接:http://www.html5jscss.com/domready.html

转载请注明《onload事件以及DOMReady事件兼容方案》| html5jscss

评论关闭了.