什么是DOMReady?
一个被封装的方法,用以当浏览器生成DOM树结构后就开始解析某些js代码。
为什么需要DOMReady?
在初学js时,我们都知道使用window.onload用以保证页面的所有资源都加载完毕后才执行当中的js代码,否则当js在title中定义或则引用时会报错:不能为空节点添加某某属性。是的,onload的确是一个简单易用的方法,但随着我们学习的深入,当我们构建的页面存在大量图片或其他需要较长时间加载的资源时,显然让js等待这些费事的资源加载完毕再执行是非常不合理且形成糟糕的用户体验。这时我们有了新的需求,需要在DOM树生成后&其他资源加载以及渲染前就执行js代码,所以DOMReady出现了。
DOMReady的原理
在说及DOMReady的原理之前,我们需要知道渲染引擎的渲染(浏览器把请求的html显示出来的过程)基本流程:
1.解析HTML,构建DOM树(构建DOM节点);
2.构建渲染树(解析样式信息,包括外部的css文件、style标签中的样式,按优先级解析)。渲染树由一些包含有各种属性的矩形组成,他们将会按照正确的顺序显示到屏幕上;
3.布局渲染树(布局DOM节点),执行布局的过程,将确定每个节点在屏幕上的确切坐标;
4.绘制渲染树(绘制DOM节点,即遍历渲染树),使用UI后端层来绘制每个节点。
解析HTML 构建DOM Tree构建渲染树(解析样式信息)布局渲染树(布局DOM节点)绘制Render Tree渲染树
Note:
DOMReady就是在 构建了DOM树之后就开始解析JS,而不是等待页面渲染完毕。
HTML是标记语言,它的本质就是告诉浏览器这里有什么节点,浏览器解析后才会变成DOM节点。
节点都是以树的形式组织的,当页面上的所有HTML都转换为节点,就称为DOMReady
浏览器解析的顺序是从上到下,从左到右。 但是如果遇到script标签或者遇到src href属性它就会引用外部资源,这就要区别对待了。 这就是阻塞了:它一定会等src指定的脚本文件加载下来,然后全部执行了里面的脚本,才会分析下一个标签。
加载iframe
其实加载iframe会不会阻塞,这篇文章已经做了介绍了:
http://www.cnblogs.com/sharpxiajun/p/4077515.html
iframe应该是不阻塞DOM构建的。
iframe如果是一个静态普通文档,没有引入任何资源,就不会阻塞。
但是如果iframe的页面中包含图片、脚本或外部CSS样式等,它会发出HTTP请求,HTTP请求是有限的,它会与父标签的其他需要加载外部资源的标签产生竞争。我们经常看到一些新闻网,上面会挂许多iframe广告, 这些页面一开始加载时就很卡,也是这缘故.
如何实现DOMReady
其实就是DOMReady就是页面的DOM树创建完成后的时候。(当页面上的所有HTML都转换为节点,就称为DOMReady)
实现一
我们可能会下意识想写一个setInerval来循环检测节点是否生成好了。
但是这种做法是充分不确定的。
很早期, 浏览器提供了一个window.onload方法,但这东西是等到所有标签变成DOM,并且外部资源,图片,背景音乐什么都加载好才触发, 时间上有点晚.
幸好,浏览器提供了一个document.readyState属性,当它变成complete时,说明这时机到了但这是一个属性,不是一个事件,需要使用不太精确的setInterval轮询
实现2
W3C提供的DOMContentLoaded事件:
//为了在DOM加载完成之后,脚本载入之前执行!
//当页面加载完成后去做一些事情:绑定事件、DOM操作某些结点等。
//window.onload是很迟的,在加载完图片脚本样式和iframe中的所有资源之后
//W3C提出的DOMContentLoaded方法与onload相比该方法触发时间更早
//页面加载完DOM后立刻触发。 IE 9+支持
//同时支持iframe document和本身的document
function myDOMReady(documentEle,fn) {
//判断如果支持addEventListten
if(documentEle.addEventListener){
documentEle.addEventListener('DOMContentLoaded',fn,false);
}else{
IEContentLoaded(fn);
}
}
对于IE而言,有一个不是W3C标准的方法,在IE11以后就不再支持了,类似 W3C中的 scrollLeft 和 scrollTop方法,当然对于要兼容IE8来说是支持的,它模拟在滚动条的一次点击。
对于IE9来说,只有Quirks模式会使用这个方法
//IE下模拟DOMContentLoaded
function IEContentLoaded(fn){
var done=false,document=window.document;
//只执行一次用户的回调函数init
var init=function(){
if(!done){
done=true;
fn();
}
}
(function(){
try{
//DOM Tree 未创建完之前调用doScroll会抛出错误
//doScroll是一个伟大的hack
document.documentElement.doScroll('left');
}catch(err){
//延时再执行
setTimeout(argument.callee, 1);
return;
}
//没有错误表示DOM树创建完毕,执行用户回调
init();
})();
//监听document的加载状态
document.onreadystatechange=function(){
//如果用户是在DOMReady之后调用的函数立即执行用户回调
if(document.readyState==='complete'){
document.onreadystatechange=null;
init();
}
}
}
其他框架库对domReady的实现也是类似原理:
关键是 设置一个数组, 当domReady这个时刻没有到时,先将回调放到数组里; 然后是各种检测domReady的方法(如DOMContentLoaded, onreadystatechange, doScroll hack, document.readyState轮询 ), 一旦到了,就执行这个数组所有回调, 并且以后用户再进入这个方法, 就不放数组,直接执行.
额外补充IE的readystatechange
IE这个事件的目的是提供与文档或元素加载状态有关的信息,但这个事件的行为有时候也很难预料。
而且仅仅是为DOM文档中某些部分提供了这个事件.
支持这个事件的对象有一个readystate属性,可能值为:
- uninitialize:对象存在但未初始化
- loading: 对象正在加载数据
- interactive loaded 可以操作对象了,但还没完全加载
- complete 对象加载完毕
但是并非所有对象都会经历这几个阶段。 显然,这意味着readyState属性值不总连续,可能会被跳过。
对于document而言,值为"interactive"的readyState会在与DOMContentLoaded大致相同的时刻触发readystatechange事件。 此时DOM Tree加载完毕了,可以安全操作它了。 因此会进入interactive阶段!
与load的顺序
这个无法确定,我们无法保证readyState为interactive的时候就一定比load早,当包含外部资源较多的时候可能是这样。
所以我们最好是同时检测交互阶段和完成阶段
//跨浏览器兼容事件处理函数
EventUtil.addHandler(document,'readystateChange',function(event){
if(document.readyState=='interactive'||document.readyState=='complete'){
EventUtil.removeHandler(document,"readystatechange",arguments.callee);
//.....逻辑
console.log('Content loaded')
}
})
这个函数是为了在交互阶段或者完成阶段中移除相应的事件处理程序,防止被执行多次。
<script> <link> 触发
在IE中,这两个元素也会触发这个readystatechange事件,用来确定外部资源是否加载完成。这种基于元素触发的事件和基于document触发的事件有点区别,就是基于元素触发的readyState属性无论等于‘loaded’ 还是‘complete’都可以表示资源已经可用。 有时候readyState会停在”loaded”阶段而永远不会完成,有时候会停在”complete”,所以两个阶段都需要判断
EventUtil.addHandler(script,'readystatechange',functiion(event){
event=EventUtil.getTarget(event);
if(target.readyState=='loaded'||target.readyStat=='complete'){//...}
});
注意这个属性在W3C标准中是作为document的属性!
标准中写明了document拥有这个属性,而一般我们还是用DOMContentLoaded事件检测。
完整源码实现:
、
//IE模拟DOMContentLoaded
function myReady(fn) {
//对于现代浏览器,对DOMContentLoaded事件处理采用标准的事件绑定方式
if(document.addEventLister){
document.addEventLister("DOMContentLoaded", fn, false);// 在冒泡阶段捕获
}else {
IEContentLoaded(fn);
}
//IE模拟DOMContentLoaded
function IEContentLoaded(fn) {
var d = window.document;
var done = false;
//只执行一次用户的回调函数init()
var init = function () {
if(!done) {
done = true;
fn();
}
};
(function (){
try {
//DOM树未创建之前调用doScroll会抛出错误
d.documentElement.doScroll('left');
}catch (e){
//延迟一次
setTimeout(arguments.callee, 50);
return;
}
//没有错误就表示DOM树创建完毕,然后立马执行用户回调
init();
}) ();
//监听document的加载状态
d.onreadystatechange = function () {
//如果用户是在domReady之后绑定的函数,就立马执行
if(d.readyState == 'complete') {
d.onreadystatechange = null;
init();
}
}
}
}