在”浏览器环境下Javascript脚本加载与执行探析“系列文章的前几篇,分别针对浏览器环境下JavaScript加载与执行相关的知识点或者属性进行了探究,感兴趣的同学可以先行阅读前几篇文章,了解相关的知识点:
(1)《浏览器环境下JavaScript脚本加载与执行探析之代码执行顺序》主要针对向页面引入JavaScript代码的不同方法,研究了其在代码执行顺序方面的问题,特别重点研究了document.write这种方式引入JavaScript脚本时的执行顺序问题。
(2)《浏览器环境下JavaScript脚本加载与执行探析之defer与async特性》讲解了两个影响脚本加载与执行顺序的<script>标签元素特性:defer和async
(3)《浏览器环境下JavaScript脚本加载与执行探析之动态脚本与Ajax脚本注入》又通过实验探究了动态脚本技术和Ajax脚本注入技术的代码执顺序问题,并与defer属性的使用做了结合的比较
本篇文章继续浏览器环境下JavaScript脚本加载与执行探析的话题,重点讲解DOMContentLoaded这一在实际编程中经常使用的事件。
相信有经验的前端开发者对DOMContentLoaded事件已经很熟悉了,MDN的解释可以很清楚地表达这个事件的意思:
The DOMContentLoaded event is fired when the initial HTML document has been completely loaded and parsed, without waiting for stylesheets, images, and subframes to finish loading. A very different event - load - should be used only to detect a fully-loaded page. It is an incredibly popular mistake to use load where DOMContentLoaded would be much more appropriate, so be cautious.
重点阅读加粗的部分,也就是说,DOMContentLoaded是在HTML文档完全加载和解析结束时触发,而不会等待样式表、图片和子框架的加载。在实际编程中,我们通常都有这样一种需求,“文档加载结束时执行某些JS代码”,较早的时候,可能会使用window.onload=function(){}这样的方法,但是onload需要等待图片、样式表等资源加载结束才会触发,如果我们的网页包含较多的这些资源,那么用户必须等待这些资源加载结束后才能与页面进行交互,明显是有违用户体验的,这一点相信前端开发者也都比较熟悉了。
DOMContendLoaded虽好,但是兼容性还是有些问题:
可以看到,IE<=8是不支持DOMContentLoaded的,那么肯定会有相应的兼容方案,我们这里讲解jQuery的兼容方法
我们都知道,在jQuery中,绑定DOMContentLoaded事件有以下两种方法:
(1)$(document).ready(function(){//要执行的代码})
(2)$(function(){//要执行的代码})
这两种写法是等价的,我们通过源码来看一下,直接写$(function(){})时,代码是如何运行的:
在jQuery1.7.1的196行:
1 // HANDLE: $(function) 2 // Shortcut for document ready 3 } else if ( jQuery.isFunction( selector ) ) { 4 return rootjQuery.ready( selector ); 5 }
当我们向jQuery的构造函数直接传入了一个函数时,会调用以上代码,而rootjQuery是什么呢?我们继续从源码中找答案:
在源码的第916行:
1 // All jQuery objects should point back to these 2 rootjQuery = jQuery(document);
这样对应起来,就可以知道为什么这两种写法是等价的了。
在源码的第83行,有如下的声明代码:
1 // The ready event handler 2 DOMContentLoaded,
这里声明了一个ready事件的处理器,并从脚本的第919行开始,对DOMContentLoaded处理器进行了定义:
1 // Cleanup functions for the document ready method 2 if ( document.addEventListener ) { 3 DOMContentLoaded = function() { 4 document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false ); 5 jQuery.ready(); 6 }; 7 8 } else if ( document.attachEvent ) { 9 DOMContentLoaded = function() { 10 // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). 11 if ( document.readyState === "complete" ) { 12 document.detachEvent( "onreadystatechange", DOMContentLoaded ); 13 jQuery.ready(); 14 } 15 }; 16 }
这里的兼容性写法相信不难理解,DOMContentLoaded被定义为一个函数,函数的功能是调用jQuery的静态方法:ready方法,同时如果浏览器支持addEventListener,那么则移除DOMContentLoaded的监听,否则如果是IE<=8,则移除document的readystatechange事件的监听。document的readyState可以参考:https://developer.mozilla.org/en-US/docs/Web/API/Document/readyState
在这段的后面,紧接着定义了一个doScrollCheck方法,这个方法是在低版本IE中辅助判断DOMContentLoaded的方法:
1 // The DOM ready check for Internet Explorer 2 function doScrollCheck() { 3 if ( jQuery.isReady ) { 4 return; 5 } 6 7 try { 8 // If IE is used, use the trick by Diego Perini 9 // http://javascript.nwbox.com/IEContentLoaded/ 10 document.documentElement.doScroll("left"); 11 } catch(e) { 12 setTimeout( doScrollCheck, 1 ); 13 return; 14 } 15 16 // and execute any waiting functions 17 jQuery.ready(); 18 }
这个方法首先判断jQuery.isReady状态,如果isReady为true,表示ready事件已经被触发,无需再进行检测。如果在一个try-catch代码块中调用document.documentElment.doScroll方法,如果这个方法不报错,那么就表示document已经完成了加载和构建,功能上类似于DOMContentLoaded;如果报错,则在catch块中连续调用自身,直到方法不报错为止,然后调用jQuery.ready方法。
2.2节中介绍的代码是jQuery脚本初始化时就会执行的代码,也就是说,在我们引入了jQuery脚本,并且浏览器解析到了相应的<script>标签,浏览器就会去下载脚本并执行脚本代码,此时,2.2节中的代码就已经执行。然后,如果我们在之后的脚本中,使用2.1节介绍的方法去绑定ready事件的处理函数,则首先会调用jQuery.prototype.ready方法,该方法定义在第273行:
1 ready: function( fn ) { 2 // Attach the listeners 3 jQuery.bindReady(); 4 5 // Add the callback 6 readyList.add( fn ); 7 8 return this; 9 },
首先调用了jQuery的静态方法bindReady,然后将回调函数加入到readyList中,readyList专门用来存储DOMContentLoaded事件的延迟函数,最后函数返回了this。
这个方法用来初始化ready事件函数列表,并为document绑定DOMContentLoaded事件,我们来看一下代码:
1 bindReady: function() { 2 if ( readyList ) { 3 return; 4 } 5 6 readyList = jQuery.Callbacks( "once memory" ); 7 8 // Catch cases where $(document).ready() is called after the 9 // browser event has already occurred. 10 if ( document.readyState === "complete" ) { 11 // Handle it asynchronously to allow scripts the opportunity to delay ready 12 return setTimeout( jQuery.ready, 1 ); 13 } 14 15 // Mozilla, Opera and webkit nightlies currently support this event 16 if ( document.addEventListener ) { 17 // Use the handy event callback 18 document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); 19 20 // A fallback to window.onload, that will always work 21 window.addEventListener( "load", jQuery.ready, false ); 22 23 // If IE event model is used 24 } else if ( document.attachEvent ) { 25 // ensure firing before onload, 26 // maybe late but safe also for iframes 27 document.attachEvent( "onreadystatechange", DOMContentLoaded ); 28 29 // A fallback to window.onload, that will always work 30 window.attachEvent( "onload", jQuery.ready ); 31 32 // If IE and not a frame 33 // continually check to see if the document is ready 34 var toplevel = false; 35 36 try { 37 toplevel = window.frameElement == null; 38 } catch(e) {} 39 40 if ( document.documentElement.doScroll && toplevel ) { 41 doScrollCheck(); 42 } 43 } 44 },
代码首先判断readyList是否初始化过了,没有初始化才继续执行。
readyList = jQuery.Callbacks( "once memory" );
这一句代码创建一个监听函数列表,这里用到了jQuery中的底层支持模块:Callbacks,向方法传入的两个flag的含义为(可以参考《jQuery技术内幕 深入解析jQuery架构设计与实现原理》4.1节):
once:表示监听函数列表readyList只能被触发一次;记录上一次触发readyList时的参数,之后添加的任何监听函数都将用记录的参数值立即调用 。
随后开始为document绑定DOMContentLoaded,第一个if分支很好理解,第一个if分支的最后一行增加load事件的监听是一种防御性编程,同时我们在创建readyList时使用了once标记,这样即使load事件触发后,readyList中已经执行过的函数都不会再执行。
如果在IE<=8中,也就是if的第二个分支,那么则同时使用两种方法模拟判断DOMContentLoaded,一种是监听readyState是否为"complete",一种则使用doScrollCheck方法,两种方法只要有一种满足条件,就会触发jQuery.ready。
接下来的核心就是jQuery.ready方方法了,我们首先看一下代码:
1 // Handle when the DOM is ready 2 ready: function( wait ) { 3 // Either a released hold or an DOMready/load event and not yet ready 4 if ( (wait === true && !--jQuery.readyWait) || (wait !== true && !jQuery.isReady) ) { 5 // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). 6 if ( !document.body ) { 7 return setTimeout( jQuery.ready, 1 ); 8 } 9 10 // Remember that the DOM is ready 11 jQuery.isReady = true; 12 13 // If a normal DOM Ready event fired, decrement, and wait if need be 14 if ( wait !== true && --jQuery.readyWait > 0 ) { 15 return; 16 } 17 18 // If there are functions bound, to execute 19 readyList.fireWith( document, [ jQuery ] ); 20 21 // Trigger any bound ready events 22 if ( jQuery.fn.trigger ) { 23 jQuery( document ).trigger( "ready" ).off( "ready" ); 24 } 25 } 26 },
这个函数有一个参数wait,主要用来处理ready事件的延迟和重新触发,它和jQuery.readyWait属性关联,这个属性默认值为1,我们暂不考虑延迟的情况,就把wait视为undefined,readyWait为1,在这种情况下,我们的代码符合第一个if条件,第6行的代码判断document.body,目的是保证document.body存在,然后再把jQuery.isReady设为true,表示document已经加载和解析完毕。第14行的if条件在这里也同样满足,接下来就通过readyList.fireWith执行readyList中的函数列表,第一个参数document指定了函数的上下文,第二个参数则指定了ready监听函数的参数。接下来的代码执行通过jQuery(document).on("ready",fn)绑定的函数。
在2.1节中提到,在jQuery中,绑定DOMContentLoaded事件由两种方法,事实上,在阅读完相应的源码之后,应该可以看到,其实还有第三种方法:
(3)$(document).on("ready", fn)
这种方法绑定的ready函数与前两种方法有些不同,如果在DOMContentLoaded事件已经触发之后,通过这种方法绑定的代码是不会再执行的,而通过前两种方法绑定的函数仍然可以执行,究其原因还是在于jQuery构建了自身的一套ready事件绑定和执行体系,Query( document ).trigger( "ready" )这段代码只在jQuery.ready方法中才会调用,而jQuery.ready方法只会在DOMContentLoaded事件触发之后才会执行(或document.readyState===“complete”或doScrollCheck不再报错),如果在DOMContentLoaded触发再绑定,那么函数时不会执行的(这与js中的事件机制是相同的,如果错过了事件再去监听,那么是不会有结果的)