前言
在浏览器把DOM树渲染好之前,javascript是无法操作没渲染好的DOM节点。
例如:
1 2 3 4 5 6 | <script> var dom = document.getElementById("test"); //由于script先执行,此时浏览器的还没渲染id为test的DOM节点,所以这里拿不到 </script> <div id="test"> </div> |
这是很多新手在操作DOM树容易犯的错:在文档DOM就绪前就取DOM节点。
jQuery提供了ready函数,你可以在ready里边添加回调,jQuery会保证在文档就绪后依次执行这些回调。
用ready接口,以上的例子就可以改成:
1 2 3 4 5 6 7 8 9 | <script> $(document).ready(function(){ var dom = document.getElementById("test"); //这里放在ready的回调里边,浏览器渲染完DOM节点后会回调此函数 //这里dom就拿到了。 }); </script> <div id="test"> </div> |
DOM渲染事件发生前后
本文并不是为了说明HTTP协议,TCP/IP怎么运作,仅仅只是把一个简单的流程呈现出来,以便能让新手看得明白浏览器基本的处理过程,进而明白ready事件。(有写错的,请指正)
- 浏览器发送www.a.com去DNS服务器查询a.com服务器的门牌号ip,假设是113.108.20.51
- 浏览器往113.108.20.51发送HTTP请求请求页面
- 服务器根据用户的请求,得知是a.com的请求,交由a.com的网关程序,这里就是交由PHP处理请求
- PHP程序会通过一系列的计算(例如操作数据库等)最后往缓冲区输出一个HTML内容
- 服务器把生成HTML内容返回到用户机器
- 用户机器收到服务器的返回,将HTML内容交由浏览器处理
- 浏览器开始解析HTML内容,生成DOM树后,触发DOMContentLoaded事件,注意到此时页面上只是DOM节点就绪了,但是js、css、图片等还处于待下载状态
- 浏览器发送请求去下载js、css、图片。全部下载结束的时候触发onload事件
注意到这里,一个服务器可以映射多个域名,这是通过虚拟主机(表面意思就是虚拟出很多台机器的样子)来管理的。
$(document).ready()
从上述流程看出,其实在DOMContentLoaded事件发生的时候,HTML的DOM结构其实就已经就绪了,这时候就可以操作DOM节点了。
jQuery就是在此时触发ready回调的,但是后边从源码可以看到,它还做了一些防备,防止某些旧版浏览器只会触发onload事件,所以它在onload事件也派发出ready事件。
ready回调的写法如下:
1 2 3 4 5 6 7 8 9 | $(document).ready(function($){ //回调,参数$为jQuery引用 }); $(function($){ //这是上边那种形式的快捷方式而已 }); $("#id").ready(function($){ //这种方式跟第一种本质是一样的,并不是代表#id这个节点ready的时候触发 }) |
再看看以下的测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <!doctype html> <html> <head> <meta charset="utf-8"> <title>测试$.ready</title> <script src="jquery.js" type="text/javascript"></script> <style> .clr{color:yellow;} </style> </head> <body> <div id="id" class="clr">测试</div> <script> var fn1 = function() { alert('加载完毕'); }; $(document).ready(fn1); </script> <!-- 这里我用了php去sleep三秒后才返回一段css代码 --> <!-- 返回css代码为:#id{color:red;} --> <link rel="stylesheet" type="text/css" href="/t/getCSS.php?sleep=3"> </body> </html> |
效果图就不粘贴出来了,出现的效果就是,浏览器先显示黄色的“测试”二字,然后alert出一个’加载完毕’,紧接着大概三秒后,测试两字变成了红色。可见jQuery的ready是在下载资源前就已经触发了。
(PS:需要注意的是,这里需要把link放到body最后边,是因为浏览器遇到css引用的时候,会停下来下载css而不会继续渲染DOM,以后再写篇文章把这些细节都测试讨论一下)
ready源码
如果你看过我之前两篇关于异步队列的文章,理解ready里边一些代码就简单很多了,以下是两篇文章地址:
《jQuery源码剖析(二)——$.Callbacks》
《jQuery源码剖析(三)——$.Deferred》
由于ready代码不算特别复杂,所以直接贴出代码注释:
1 2 3 4 5 6 7 8 9 10 11 12 13 | //可以看出ready回调是绑定在jQuery的实例上的 //$(document).ready(fn) //$("#id").ready(fn) //都调用此处 jQuery.fn.ready = function( fn ) { // Add the callback //这里的jQuery.ready.promise()返回的就是《jQuery源码剖析(三)——$.Deferred》所说的异步队列 //调用异步队列的done方法,把fn回调加入成功队列里边去 jQuery.ready.promise().done( fn ); //支持jQuery的链式操作 return this; }, |
有点绕的就是这里的jQuery.ready是一个辅助函数(注意区别jQuery.fn.ready),但同时它也是一个对象,jQuery里边就直接把辅助函数promise挂在它下边了
接着先看看jQuery.ready.promise里边实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | jQuery.ready.promise = function( obj ) { //这里的obj是可以扩展返回的异步队列的, //详细见《jQuery源码剖析(三)——$.Deferred》 //readyList就是内部用于ready的一个异步队列 if ( !readyList ) { //如果没有,就用jQuery.Deferred()生成一个异步队列 readyList = jQuery.Deferred(); // Catch cases where $(document).ready() is called after the browser event has already occurred. // we once tried to use readyState "interactive" here, but it caused issues like the one // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15 // Chrome下document.readyState == interactive // IE下document.readyState == complete //这里是针对IE做的 if ( document.readyState === "complete" ) { // Handle it asynchronously to allow scripts the opportunity to delay ready //这里的setTimeout是为了异步 setTimeout( jQuery.ready, 1 ); // Standards-based browsers support DOMContentLoaded //标准浏览器侦听事件接口:document.addEventListener } else if ( document.addEventListener ) { // Use the handy event callback document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); // A fallback to window.onload, that will always work //文章一开始说了,这里为了保证一定会触发ready,所以还针对onload也绑定一次回调 window.addEventListener( "load", jQuery.ready, false ); // If IE event model is used } else { //IE侦听事件接口:document.attachEvent //如果有onreadystatechange事件,侦听之 // 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 ); // http://javascript.nwbox.com/IEContentLoaded/ // 见下边说明 // If IE and not a frame // continually check to see if the document is ready var top = false; try { top = window.frameElement == null && document.documentElement; } catch(e) {} //如果是IE并且不是iframe if ( top && top.doScroll ) { (function doScrollCheck() { if ( !jQuery.isReady ) { try { // Use the trick by Diego Perini // http://javascript.nwbox.com/IEContentLoaded/ //一直调用doScroll滚动,因为DOM渲染结束前,DOM节点是没有doScroll方法的,所以一直会异常 //直到DOM渲染结束了,这个时候doScroll方法不会抛出异常,然后就调用$.ready() top.doScroll("left"); } catch(e) { return setTimeout( doScrollCheck, 50 ); } // and execute any waiting functions jQuery.ready(); } })(); } } } //调用异步队列的promise方法隐藏一些触发事件接口 //因为触发ready事件的控制权应该是jQuery自己管理 return readyList.promise( obj ); }; //其中绑定的ready回调 // The ready event handler and self cleanup method var DOMContentLoaded = function() { if ( document.addEventListener ) { //标准浏览器 document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false ); jQuery.ready();//最后ready事件触发时,执行的是jQuery.ready(),注意区分jQuery.fn.ready() } else if ( document.readyState === "complete" ) { //IE下ready时:document.readyState === "complete" // we're here because readyState === "complete" in oldIE // which is good enough for us to call the dom ready! document.detachEvent( "onreadystatechange", DOMContentLoaded ); jQuery.ready(); } } |
接着看最后调用的ready部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | $.extend(jQuery,{ // Is the DOM ready to be used? Set to true once it occurs. //当前文档是否已经ready了 isReady: false, // A counter to track how many items to wait for before // the ready event fires. See #6781 //可以使用holdReady接口来hold住ready事件 readyWait: 1, // Hold (or release) the ready event //可以使用holdReady接口来hold住ready事件 //参数hold为true时,jQuery会hold住ready回调。 //某个地方调用了holdReady(),参数hold为false,jQuery会把hold住的次数减1 holdReady: function( hold ) { if ( hold ) { jQuery.readyWait++; } else { jQuery.ready( true ); } }, // Handle when the DOM is ready ready: function( wait ) { //如果不需要等待,holdReady()的时候,把hold住的次数减1,如果还没到达0,说明还需要继续hold住,return掉 //如果不需要等待,判断是否已经Ready过了,如果已经ready过了,就不需要处理了。异步队列里边的done的回调都会执行了 // Abort if there are pending holds or we're already ready if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { return; } // 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 //此时Ready了 jQuery.isReady = true; // If a normal DOM Ready event fired, decrement, and wait if need be //当时如果此时有某个地方hold住了,那就要继续等待 if ( wait !== true && --jQuery.readyWait > 0 ) { return; } // If there are functions bound, to execute //调用异步队列,然后派发成功事件出去(最后使用done接收,详细见《jQuery源码剖析(三)——$.Deferred》),把上下文切换成document,默认第一个参数是jQuery。 //还记得 $(document).ready(function($){})这里的$不? readyList.resolveWith( document, [ jQuery ] ); // Trigger any bound ready events //最后jQuery还可以触发自己的ready事件 //例如: // $(document).on('ready', fn2); // $(document).ready(fn1); //这里的fn1会先执行,自己的ready事件绑定的fn2回调后执行 if ( jQuery.fn.trigger ) { jQuery( document ).trigger("ready").off("ready"); } } }); |
文中有误的地方还请指正。欢迎交流~
本文链接:jQuery源码剖析(四)——$(document).ready
转载声明:本博客文章若无特别说明,皆为原创,转载请注明来源:拉风的博客,谢谢!
张杨
nice job!