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

  • 相关阅读:
    Java基础加强--17.利用反射操作泛型VI【泛型类型变量的语义】【GenericDeclaration接口】【泛型接口TypeVariable】【通过Class反射解析泛型类】
    [G]ParameterizedType详解
    java 泛型详解-绝对是对泛型方法讲解最详细的,没有之一
    source insight遇到__attribute__解析不到函数
    Exif文件格式描述
    opencv python 匹配
    SQL知识累积
    ant 安装过程中问题记录
    useful-scripts
    excel 经验总结
  • 原文地址:https://www.cnblogs.com/tracylin/p/5353447.html
Copyright © 2011-2022 走看看