你的位置:首页 > Java教程

[Java教程]浏览器环境下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 ready3     } else if ( jQuery.isFunction( selector ) ) {4       return rootjQuery.ready( selector );5     }

 当我们向jQuery的构造函数直接传入了一个函数时,会调用以上代码,而rootjQuery是什么呢?我们继续从源码中找答案:

在源码的第916行:

1 // All jQuery objects should point back to these2 rootjQuery = jQuery(document);

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

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

在源码的第83行,有如下的声明代码:

1   // The ready event handler2   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 functions17   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 listeners3     jQuery.bindReady();4 5     // Add the callback6     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 ready12       return setTimeout( jQuery.ready, 1 );13     }14 15     // Mozilla, Opera and webkit nightlies currently support this event16     if ( document.addEventListener ) {17       // Use the handy event callback18       document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );19 20       // A fallback to window.onload, that will always work21       window.addEventListener( "load", jQuery.ready, false );22 23     // If IE event model is used24     } else if ( document.attachEvent ) {25       // ensure firing before onload,26       // maybe late but safe also for iframes27       document.attachEvent( "onreadystatechange", DOMContentLoaded );28 29       // A fallback to window.onload, that will always work30       window.attachEvent( "onload", jQuery.ready );31 32       // If IE and not a frame33       // continually check to see if the document is ready34       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 ready11       jQuery.isReady = true;12 13       // If a normal DOM Ready event fired, decrement, and wait if need be14       if ( wait !== true && --jQuery.readyWait > 0 ) {15         return;16       }17 18       // If there are functions bound, to execute19       readyList.fireWith( document, [ jQuery ] );20 21       // Trigger any bound ready events22       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中的事件机制是相同的,如果错过了事件再去监听,那么是不会有结果的)