1、扯淡部分javascript
回想当年,在摆脱写页面时js全靠从各类DEMO中copy出来而后东拼西凑的幽暗岁月以后,毅然决然地打算放弃这种到处“拿来主义”的不正之风,而后开启通往高大上的“前端攻城狮”的飞升之旅。想一想都有些小激动呢~然而人生不如意者十之八九,刚踏上征程就常常会被各类Error虐到体无完肤,有时候甚至会被在如今看来很低级的bug折磨得生不如死。但没有一种成长是不须要付出代价的,也就是那段刚跳入泥潭的日子开启了让本身成为一名真正的JSer的大门,也使本身在奔向高大上的路上让“见招拆招、兵来将挡”成为常态,以致于后来都慢慢以为,作一个东西不赶上几个bug内心就没有稳妥扎实的安全感。再后来也就学着不断去安慰本身:踩到脚底下的bug越多,离翻过那座墙也就不远了~html
回望一路走来的林林种种,有一个bug大概是每一个JSer在初入大门时都遇到过的。那就是用js获取页面元素的时候常常会报出一个TypeError:Cannot read property ‘XXX’of null.大意就是根本就没找到你要找的元素,更别说你要对它进行操做了。明明页面上有这个元素,但在js里恰恰获取不到,这让不少刚接触js不久的童鞋都伤透了脑筋,因而疯狂百度谷歌,最后才发现形成这个低级bug的始做俑者居然是window.onload,也就是文档未就绪,DOM树尚未建完就开始对节点进行操做从而致使的错误。前端
扯了那么多,终于扯到跟本文主题相干的东西了:domReady,也就是所谓的“文档就绪”。咱们对DOM节点的任何操做在DOM树建立以后就能够进行。在理解这个概念以前,咱们先来看看浏览器在载入一个文档时是怎么对HTML进行解析的。java
2、浏览器渲染引擎的HTML解析流程chrome
何谓“渲染”,其实就是浏览器把请求到的HTML内容显示出来的过程。渲染引擎首先经过网络得到所请求文档的内容,一般以8K分块的方式完成。下面是渲染引擎在取得内容以后的基本流程:后端
1,解析html以构建dom树(构建DOM节点):渲染引擎开始解析html,并将标签转化为内容树中的dom节点。promise
2,构建render树(解析样式信息):解析外部CSS文件及style标签中的样式信息。Render树由一些包含有各类属性的矩形组成,它们将被按照正确的顺序显示到屏幕上。浏览器
3,布局render树(布局DOM节点):执行布局过程,它将肯定每一个节点在屏幕上的确切坐标。安全
4,绘制render树(绘制DOM节点):Render树构建好了以后,将会再下一步就是绘制,即遍历render树,并使用UI后端层绘制每一个节点。ruby
以上就是HTML渲染的基本流程(详情请移步至“浏览器内部工做原理”),但这并不包含解析过程当中浏览器加载外部资源如图片、脚本、iframe等的过程。说白了,上面的四步仅仅是HTML结构的渲染流程,而外部资源的加载在HTML结构的渲染流程中贯穿始终,即使绘制DOM节点已经完成,外部资源依然可能正在加载中或还没有加载。
3、window.onload
了解了浏览器渲染引擎的HTML解析流程,咱们就回到domReady。前文提到了,那个蛋疼的TypeError是因为在DOM树构建完成以前对节点进行了操做,而一般的解决的办法就是让js在window.onload的回调里执行,也就是说,在文档全部的解析渲染、资源加载完成以前,不让js脚本执行,这样一来就妥妥地避免了因js操做先于DOM树建立而带来的bug:
1 Window.onload = function(){ 2 //doSomething 3 }
这样的解决办法应该是初学原生js时不少人最经常使用的解决办法,看起来也的确没什么问题。若是文档外部资源很少的时候也没什么问题,但,咱们来作一个假设。假设一个页面上有100张远程图片,我须要让js作到在点击每张图片时alert出图片的src属性,又该怎么作?
是否是已经发现点小问题了?按照第二部份内容对浏览器解析渲染HTML流程的介绍,DOM树很快就构建完毕了,而100张图片还在缓慢地加载。而要想执行alert出图片src属性的js,则须要等到100张图片所有加载完成后才能执行。而在这期间,页面元素不会响应你的任何操做,就好像“死”了同样。若是是在实际项目中,用户极可能不会等到你页面全部东东加载完之后才去操做,在面对一个不会对本身的操做作任何响应的页面,惟一比较解气的方式就是——果断关掉~而后……就没有了而后。
因此在实际应用中,咱们常常会遇到这样的场景,让页面加载后去作一些事情:绑定事件、DOM操做某些结点等。使用window.onload对于不少实际的应用而言有点太“迟”了,比较影响用户体验。那有没有更好的方法解决这个问题?好比提早到只要DOM树建立完成以后就能够进行如上操做呢?答案固然是有的:DOMContentLoaded事件。
4、DOMContentLoaded
说这个以前必需要提一下jQuery中的domReady机制。不少时候在使用jq也会出现最前面出现的那个TypeError,解决办法就是把js放到jQuery的ready回调里:
1 $(document).ready(function(){...});
或者:
1 $(function(){...});
这样一来,错误妥妥地没了。而后对比因果关系,大概得出一个结论:jQuery的ready回调应该跟window.onload的效果原理是同样的。恩,应该是这样。那咱们就先来看一看jQuery(1.11.1)的ready回调是如何实现的:
1 jQuery.fn.ready = function( fn ) { 2 // Add the callback 3 jQuery.ready.promise().done( fn ); 4 return this; 5 }; 6 jQuery.ready.promise = function( obj ) { 7 if ( !readyList ) { 8 readyList = jQuery.Deferred(); 9 if ( document.readyState === "complete" ) { 10 setTimeout( jQuery.ready ); 11 } else if ( document.addEventListener ) { 12 document.addEventListener( "DOMContentLoaded", completed, false ); 13 window.addEventListener( "load", completed, false ); 14 } else { 15 document.attachEvent( "onreadystatechange", completed ); 16 window.attachEvent( "onload", completed ); 17 var top = false; 18 try { 19 top = window.frameElement == null && document.documentElement; 20 } catch(e) {} 21 if ( top && top.doScroll ) { 22 (function doScrollCheck() { 23 if ( !jQuery.isReady ) { 24 try { 25 // Use the trick by Diego Perini 26 top.doScroll("left"); 27 } catch(e) { 28 return setTimeout( doScrollCheck, 50 ); 29 } 30 detach(); 31 jQuery.ready(); 32 } 33 })(); 34 } 35 } 36 } 37 return readyList.promise( obj ); 38 };
看起来比想象中的window.onload要复杂呵。Jq的源码中出现了DOMContentLoaded、readyState、onreadystatechange,这些跟domReady有什么关系?
咱们仍是先从DOMContentLoaded提及吧。就如前面所述,不少时候咱们会把js逻辑写在window.onload回调中,以防DOM树尚未建完就开始对节点进行操做从而致使错误,而对于不少实际应用来讲,越早介入对DOM的干涉就越好,好比进行特征侦测、事件绑定、DOM操做神马的。domReady还能够知足用户提早绑定事件的需求,由于有些状况下页面的图片等外部资源过多,window.onload迟迟不能触发,这时若尚未绑定事件,用户点任何的按钮都没反应(连接除外)会直接影响体验。
为了解决window.onload的短板,FF中便增长了一个DOMContentLoaded方法,与onload相比,DOMContentLoaded方法触发的时间更早,它是在页面的DOM树建立完成后(也就是HTML解析第一步完成)即触发,而无需等待其余资源的加载。Webkit引擎从版本525(Webkit nightly 1/2008:525+)开始也引入了该事件,Opera中也包含该方法。到目前为止NB的IE仍然没有要添加的意思。虽然IE下没有,但解决办法老是有的。因而对于那些忙前忙后的兼容小达人和死不悔改的顽固派,也就有了两套策略:
1)支持DOMContentLoaded事件的,就使用DOMContentLoaded事件;
2)不支持的,就用来自Diego Perini发现的著名Hack兼容。兼容原理大概就是,经过IE中的document.documentElement.doScroll(‘left’)来判断DOM树是否建立完毕。
Blabla了这么多,来看个IE模拟DOMContentLoaded例子吧。这个例子就来自上面发现IE下doScroll Hackd的做者,细看也就是简化版的jQuery.ready回调的IE处理逻辑。
1 function IEContentLoaded (w, fn) { 2 var d = w.document, done = false, 3 // 只执行一次用户的回调函数init() 4 init = function () { 5 if (!done) { 6 done = true; 7 fn(); 8 } 9 }; 10 (function () { 11 try { 12 // DOM树未建立完以前调用doScroll会抛出错误 13 d.documentElement.doScroll('left'); 14 } catch (e) { 15 //延迟再试一次~ 16 setTimeout(arguments.callee, 50); 17 return; 18 } 19 // 没有错误就表示DOM树建立完毕,而后立马执行用户回调 20 init(); 21 })(); 22 //监听document的加载状态 23 d.onreadystatechange = function() { 24 // 若是用户是在domReady以后绑定的函数,就立马执行 25 if (d.readyState == 'complete') { 26 d.onreadystatechange = null; 27 init(); 28 } 29 }; 30 }
而对于高大上的chrome、ff等高级浏览器来讲,对DOMContentLoaded事件的处理就相对来讲小case了,按照标准的事件绑定方式就能够处理:
1 if ( document.addEventListener ) { 2 document.addEventListener( "DOMContentLoaded", completed, false ); 3 }
5、实例
看到这,想必你们已经对DOMContentLoaded已经有了新的认识,onload保险丝也该适时换成智能电门啦~接下来就来个鲜活的例子,来让你们更清晰的作下对比。DEMO在这里~
首先,页面上有一组图片:
1 <ul> 2 <li><img src="img/01.jpg" /></li> 3 <li><img src="img/02.jpg" /></li> 4 <li><img src="img/03.jpg" /></li> 5 <li><img src="img/04.jpg" /></li> 6 <li><img src="img/05.jpg" /></li> 7 </ul>
页面的js处理逻辑:
1 <script> 2 var d = document; 3 var msgBox = d.getElementById("showMsg"); 4 var imgs = d.getElementsByTagName("img"); 5 var time1 = null,time2 = null; 6 if(d.addEventListener){ 7 d.addEventListener("DOMContentLoaded",domReady,false); 8 }else{ 9 IEContentLoaded(domReady); 10 } 11 function domReady(){ 12 msgBox.innerHTML += "dom已加载!<br>"; 13 time1 = new Date().getTime(); 14 msgBox.innerHTML += "时间戳:" + time1 + "<br>"; 15 } 16 17 //兼容IE的domReady 18 function IEContentLoaded(fn){ 19 var done = false, 20 init = function(){ 21 if(!done){ 22 done = true; 23 fn(); 24 } 25 }; 26 (function(){ 27 try { 28 d.documentElement.doScroll('left'); 29 }catch(e){ 30 setTimeout(arguments.callee,50); 31 return; 32 } 33 init(); 34 })(); 35 d.onreadystatechange = function(){ 36 msgBox.innerHTML += "加载状态:" + d.readyState + "<br>"; 37 if(d.readyState == 'complete'){ 38 d.onreadystatechange = null; 39 } 40 } 41 } 42 window.onload = function(){ 43 msgBox.innerHTML += "onload已加载!<br>"; 44 time2 = new Date().getTime(); 45 msgBox.innerHTML += "时间戳:" + time2 + "<br>"; 46 msgBox.innerHTML +="domReady比onload快:" + (time2 - time1) + "ms<br>"; 47 } 48 </script>
相信js脚本不用作过多解释,在前面都已作过详细分析,咱们直接来看运行结果:
很容易就能看出,DOMContentLoaded执行5238ms以后才执行的onload。这只是一个DEMO的差距,而若是是更大型的应用,可能这个时间差距会更大。
最后
这就是本文所分享的domReady引入的机制,有兴趣的能够继续移步以下连接。但愿本文能为你提供到帮助,也但愿与读者多多交流。如文中内容有误,请评论告知~谢谢。
浏览器内部工做原理:
http://kb.cnblogs.com/page/129756/
司徒正美《javascript的事件加载》:
http://www.cnblogs.com/rubylouvre/archive/2009/08/26/1554204.html