浏览器环境下Javascript脚本加载与执行探析之DOMContentLoaded

在”浏览器环境下Javascript脚本加载与执行探析“系列文章的前几篇,分别针对浏览器环境下JavaScript加载与执行相关的知识点或者属性进行了探究,感兴趣的同学可以先行阅读前几篇文章,了解相关的知识点:

(1)《浏览器环境下JavaScript脚本加载与执行探析之代码执行顺序》主要针对向页面引入JavaScript代码的不同方法,研究了其在代码执行顺序方面的问题,特别重点研究了document.write这种方式引入JavaScript脚本时的执行顺序问题。

(2)《浏览器环境下JavaScript脚本加载与执行探析之defer与async特性》讲解了两个影响脚本加载与执行顺序的<script>标签元素特性:defer和async

(3)《浏览器环境下JavaScript脚本加载与执行探析之动态脚本与Ajax脚本注入》又通过实验探究了动态脚本技术和Ajax脚本注入技术的代码执顺序问题,并与defer属性的使用做了结合的比较

本篇文章继续浏览器环境下JavaScript脚本加载与执行探析的话题,重点讲解DOMContentLoaded这一在实际编程中经常使用的事件。

1. 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的兼容方法

2. jQuery中的DOMContentLoaded兼容(以jQuery1.7.1为例)

2.1 jQuery.prototype.ready方法

我们都知道,在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);

 这样对应起来,就可以知道为什么这两种写法是等价的了。

2.2 jQuery脚本初始化时做了什么

在源码的第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.3 jQuery.prototype.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。

2.4 jQuery.bindReady方法

这个方法用来初始化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。

2.5 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)绑定的函数。

3 补充说明

在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中的事件机制是相同的,如果错过了事件再去监听,那么是不会有结果的)

 

posted @ 2016-04-05 16:12  TracyLin  阅读(1175)  评论(1编辑  收藏  举报