jQuery实现DOM加载方法源码分析
DOM是文档对象模型,DOM可以以一种独立于平台和语言的方式访问和修改一个文档的内容和结构。本文我们讲讲jQuery实现DOM加载方法源码分析。
传统的判断dom加载的方法.
使用 dom0级 onload事件来进行触发所有浏览器都支持在最初是很流行的写法 我们都熟悉这种写法:
- window.onload=function(){
- ...
- }
但是onload事件触发过于缓慢,尤其是在存在很多外部图片或者视频文件的时候,为了更好的了解这一点有必要知道一个html文档是如何进行加载的,这里引用一个园友的表述:
1.用户输入网址(假设是个html页面,并且是第一次访问),浏览器向服务器发出请求,服务器返回html文件;
2.浏览器开始载入html代码,发现标签内有一个标签引用外部CSS文件;
3.浏览器又发出CSS文件的请求,服务器返回这个CSS文件;
4.浏览器继续载入html中部分的代码,并且CSS文件已经拿到手了,可以开始渲染页面了;
5.浏览器在代码中发现一个标签引用了一张图片,向服务器发出请求。此时浏览器不会等到图片下载完,而是继续渲染后面的代码;
6.服务器返回图片文件,由于图片占用了一定面积,影响了后面段落的排布,因此浏览器需要回过头来重新渲染这部分代码;
7.浏览器发现了一个包含一行Javascript代码的标签,赶快运行它;
8.Javascript脚本执行了这条语句,它命令浏览器隐藏掉代码中的某个
9.终于等到了的到来,浏览器泪流满面……
10.等等,还没完,用户点了一下界面中的“换肤”按钮,Javascript让浏览器换了一下标签的CSS路径;
11.浏览器召集了在座的各位
- 们,“大伙儿收拾收拾行李,咱得重新来过……”,浏览器向服务器请求了新的CSS文件,重新渲染页面。
可以看到是先加载dom结构后加载对用的资源 比如一个一个img标签 ,浏览器再加载img标签时不会等到src对应的图片加载完成就会执行后面的代码,而onload则必须要等到所有资源加载完成才会触发,所以domContentLoaded 就代替了onload 但是对于ie低版本浏览器来说这种方法还没有实现 ,那么如何实现完美的判断dom加载呢?下面来看jquery的写法:
使用jquery进行开发
- <span style="font-size: 16px; font-family: 'Microsoft YaHei';">$(function(){
- ...
- })
- //or
- $(doucment).ready(function(){
- ...
- })</span>
在稍后的分析中会发现两者并无区别,下面就从这个入口开始一步一步了解jquery的写法:
源码分析
首先$(fn) 是在构造函数里传入了一个函数 在init函数
- // HANDLE: $(function)
- // Shortcut for document ready
- } else if ( jQuery.isFunction( selector ) ) {
- return rootjQuery.ready( selector );
- }
如果传入的是一个函数 则会执行 rootjQuery.ready( selector ); rootjQuery是什么呢?
// All jQuery objects should point back to these
rootjQuery = jQuery(document);
其实就是$(document) ,然后执行了一个原型方法ready把函数作为参数传了进去,好的现在视线转移到ready(此方法是原型方法还有工具方法不要混淆)
- ready: function( fn ) {
- // Attach the listeners
- jQuery.bindReady();
- // Add the callback
- readyList.add( fn );
- return this;
- },
fn接受了传递进来的函数 先执行了一个工具方法bindReady,视线接着转移
- bindReady: function() {
- if ( readyList ) {
- return;
- }
- readyList = jQuery.Callbacks( "once memory" );
- // Catch cases where $(document).ready() is called after the
- // browser event has already occurred.
- if ( document.readyState === "complete" ) {
- // Handle it asynchronously to allow scripts the opportunity to delay ready
- return setTimeout( jQuery.ready, 1 );
- }
- // Mozilla, Opera and webkit nightlies currently support this event
- if ( document.addEventListener ) {
- // Use the handy event callback
- document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
- // A fallback to window.onload, that will always work
- window.addEventListener( "load", jQuery.ready, false );
- // If IE event model is used
- } else if ( document.attachEvent ) {
- // ensure firing before onload,
- // maybe late but safe also for iframes
- document.attachEvent( "onreadystatechange", DOMContentLoaded );
- // A fallback to window.onload, that will always work
- window.attachEvent( "onload", jQuery.ready );
- // If IE and not a frame
- // continually check to see if the document is ready
- var toplevel = false;
- try {
- toplevel = window.frameElement == null;
- } catch(e) {}
- if ( document.documentElement.doScroll && toplevel ) {
- doScrollCheck();
- }
- }
- },
这个方法看起来复杂,呵呵不要着急一行一行的看 我们现在的分析路线是 $(fn)->$(document).ready->$.bindReady
- if ( readyList ) {
- return;
- }
这里出现了一个新变量readyList 第一次执行的时候由于只有声明没有初始化肯定是undefined所以不会走这里
// The deferred used on DOM ready
readyList,
readyList = jQuery.Callbacks( "once memory" );
然后给readyList赋值 其最为成为了一个回调对象 关于jquery回调对象的方法这里不再赘述,回调对象创建了但是目前是没有添加回调方法的
- // Catch cases where $(document).ready() is called after the
- / browser event has already occurred.
- if ( document.readyState === "complete" ) {
- // Handle it asynchronously to allow scripts the opportunity to delay ready
- return setTimeout( jQuery.ready, 1 );
- }
document.readyState表示文档加载的状态,如果加载完成了则可以直接执行ready方法也是也就是执行传递的回调函数,既然已经记载好了就可以直接执行了,使用settimeout是保证函数可以异步加载
接来下来的事情就是用dom2级事件处理程序来监听onload事件 和 domcontentLoaded 既然后者加载速度比前者快为什吗还要多此一举呢?这是因为浏览器可能会缓存事件处理程序onload可能会被缓存而先执行所以都写上谁先触发谁先执行;
只不过对于domcontentLoaded是执行的domcontentLoaded方法而不是ready方法,其实domcontentLoaded方法也是最终执行ready方法 :
- // Cleanup functions for the document ready method
- if ( document.addEventListener ) {
- DOMContentLoaded = function() {
- document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false );
- jQuery.ready();
- };
- } else if ( document.attachEvent ) {
- DOMContentLoaded = function() {
- // Make sure body exi msts, at least, in case IE gets a little overzealous (ticket #5443).
- if ( document.readyState === "complete" ) {
- document.detachEvent( "onreadystatechange", DOMContentLoaded );
- jQuery.ready();
- }
- };
- }
只不过是先解除绑定之后再执行确保不会多次触发,对于ie浏览器还有一个特殊的方法就是检测滚动条是可以可以执行 当然前提不在框架页面 ,因为如果dom结构加载好了body才有滚动条
- if ( document.documentElement.doScroll && toplevel ) {
- doScrollCheck();
- }
- // The DOM ready check for Internet Explorer
- function doScrollCheck() {
- if ( jQuery.isReady ) {
- return;
- }
- try {
- // If IE is used, use the trick by Diego Perini
- // http://javascript.nwbox.com/IEContentLoaded/
- document.documentElement.doScroll("left");
- } catch(e) {
- setTimeout( doScrollCheck, 1 );
- return;
- }
- // and execute any waiting functions
- jQuery.ready();
- }
isReady是判断是否已经加载的状态值 只有执行ready工具方法后才会变成true,然后就是不停的检测滚动条 直不报错了执行ready方法;
所以bindReady方法就是一个准备方法,把要执行的函数绑定在回调函数中并且判断何时才能去触发,最终都执行$.ready方法 注意这里的ready是工具方法 不同于上面说的ready原型方法或者叫实例方法
马上就可以看到函数被触发了但是别着急 还没有把传进来的fn添加到回调函数列表中呢,看完bindReady之后我们再回到ready实例方法中
- ready: function( fn ) {
- // Attach the listeners
- jQuery.bindReady();
- // Add the callback
- readyList.add( fn );
- return this;
- },
原来是在这里添加的 由于bindReady中调用jQuery.ready时都是采用的异步所以完全添加操作得以在执行之前完成 ,现在可以看最后工具方法ready了吧?当然不是你还要直到另一个方法holdReady
- // Hold (or release) the ready event
- holdReady: function( hold ) {
- if ( hold ) {
- jQuery.readyWait++;
- } else {
- jQuery.ready( true );
- }
- },
代码不多主要就是阻止回调函数触发的,比如我们在代码中间需要加载一个脚本文件并且希望优先于rady事件执行就可以使用此方法先停止执行后再恢复实现动态脚本加载参数为false如果不传就是组织ready事件如果传入就是解除阻止,准备工作终于完成下面开始看jQuery.ready方法:
- // Handle when the DOM is ready
- ready: function( wait ) {
- // Either a released hold or an DOMready/load event and not yet ready
- if ( (wait === true && !--jQuery.readyWait) || (wait !== true && !jQuery.isReady) ) {
- // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
- if ( !document.body ) {
- return setTimeout( jQuery.ready, 1 );
- }
此方法接受一个参数也就是holdReady可能传入的true 这里限制了两个条件才能继续运行 1,wait为true readyWait减一后为0,readyWait是一个计数器,因为holdReady可以执行多次,没执行一次该值加一解除一次该值减一 2,wait不为true 并且isRead为false 因为isReady只用执行到这条if语句后面才能修改为ture所以这是保证不要重复执行的 。正常情况下(没有调用holdReady)都是可以通过的,如果调用了并且wait存在说明有解除但是如果解除次数低于阻止次数还是不行的;
if进来之后又是一个if判断这里是这对ie的一个bug可以忽视 有兴趣查看jQuery官网说明http://bugs.jquery.com/ticket/5443 下面就可以让isReady为true了
- // Remember that the DOM is ready
- jQuery.isReady = true;
- // If a normal DOM Ready event fired, decrement, and wait if need be
- if ( wait !== true && --jQuery.readyWait > 0 ) {
- return;
- }
ready状态改变之后并不意味着可以立刻执行回调函数了,在前面判断了没有使用holdReady以及使用了holdReady(false)的情况 这两种情况仅仅可以满足isReady为ture 但是如果使用了holdReady没有传值的情况时只要readyWait减一后大于0还是不能执行但是下次解除时isReady状态已经是true了
- // If there are functions bound, to execute
- readyList.fireWith( document, [ jQuery ] );
- // Trigger any bound ready events
- if ( jQuery.fn.trigger ) {
- jQuery( document ).trigger( "ready" ).off( "ready" );
- }
最终创建的回调对象通过fireWith方法执行了,并且把this指向了doument并且把jQuery作为参数传递了进去 最后针对有可能使用 on方法绑定ready事件也进行了trigger触发然后解除绑定;至此完毕 机构比较复杂需要看着源码多理几次,最后贴上主要源码
- // Is the DOM ready to be used? Set to true once it occurs.
- isReady: false,
- // A counter to track how many items to wait for before
- // the ready event fires. See #6781
- readyWait: 1,
- // Hold (or release) the ready event
- holdReady: function( hold ) {
- if ( hold ) {
- jQuery.readyWait++;
- } else {
- jQuery.ready( true );
- }
- },
- // Handle when the DOM is ready
- ready: function( wait ) {
- // Either a released hold or an DOMready/load event and not yet ready
- if ( (wait === true && !--jQuery.readyWait) || (wait !== true && !jQuery.isReady) ) {
- // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
- if ( !document.body ) {
- return setTimeout( jQuery.ready, 1 );
- }
- // Remember that the DOM is ready
- jQuery.isReady = true;
- // If a normal DOM Ready event fired, decrement, and wait if need be
- if ( wait !== true && --jQuery.readyWait > 0 ) {
- return;
- }
- // If there are functions bound, to execute
- readyList.fireWith( document, [ jQuery ] );
- // Trigger any bound ready events
- if ( jQuery.fn.trigger ) {
- jQuery( document ).trigger( "ready" ).off( "ready" );
- }
- }
- },
- bindReady: function() {
- if ( readyList ) {
- return;
- }
- readyList = jQuery.Callbacks( "once memory" );
- //
- if ( document.readyState === "complete" ) {
- // Handle it asynchronously to allow scripts the opportunity to delay ready
- return setTimeout( jQuery.ready, 1 );
- }
- // Mozilla, Opera and webkit nightlies currently support this event
- if ( document.addEventListener ) {
- // Use the handy event callback
- document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
- // A fallback to window.onload, that will always work
- window.addEventListener( "load", jQuery.ready, false );
- // If IE event model is used
- } else if ( document.attachEvent ) {
- // ensure firing before onload,
- // maybe late but safe also for iframes
- document.attachEvent( "onreadystatechange", DOMContentLoaded );
- // A fallback to window.onload, that will always work
- window.attachEvent( "onload", jQuery.ready );
- // If IE and not a frame
- // continually check to see if the document is ready
- var toplevel = false;
- try {
- toplevel = window.frameElement == null;
- } catch(e) {}
- if ( document.documentElement.doScroll && toplevel ) {
- doScrollCheck();
- }
- }
- },