上篇博客说过脚本后置可使页面更快的加载,但是这样的优化仍是有限的,若是脚本须要执行一个耗时的操做,就算后置了它仍是会阻塞后续脚本加载和执行而且阻塞整个页面。下面介绍非阻塞加载脚本技术也就是异步加载。javascript
1.defer(关于defer的一篇好文)
目前全部浏览器都支持defer属性,可是Chrome和Firefox中只有在加载外部脚本时defer才会生效,行内脚本使用defer是没有做用的。而IE中不论什么状况,defer都有效。
defer的做用就是阻止脚本在下载完成后马上执行,它会让脚本延迟到全部脚本加载执行完成后,在DOMContentLoaded以前执行,通俗的说就是顺序加载延迟执行。虽然都是在DOMContentLoaded以前执行,可是在不一样浏览器之间,执行的各类脚本执行的顺序仍是不同的。看下面这个例子:html
<html> <meta charset="utf-8"> <head> <script type="text/javascript"> var result = "" ; var head = document.getElementsByTagName("head")[0] ; //DOMContentLoaded if(window.addEventListener){ document.addEventListener("DOMContentLoaded",function(){ result += "DOMContentLoaded\n" ; }) ; }else{ document.attachEvent("onDOMContentLoaded",function(){ result += "DOMContentLoaded\n" ; }) ; } window.onload = function(){ result += "window loaded\n"; //console.log("window loaded") ; } ; </script> <!--头部行内延迟脚本--> <script type="text/javascript" defer = "defer"> result += "Head Inline Script defer\n" ; </script> <!--头部行内脚本--> <script type="text/javascript"> result += "Head Inline Script\n" ; </script> <!--头部外部延迟脚本 External Head Script defer--> <script type="text/javascript" src = "external_head_defer.js" defer="defer"></script> <!--头部行内脚本 External Head Script--> <script type="text/javascript" src = "external_head.js"></script> </head> <body> <button>SHOW</button> <!--Body行内延迟脚本--> <script type="text/javascript" defer = "defer"> result += "Body Inline Script defer\n" ; </script> <!--Body行内脚本--> <script type="text/javascript"> result += "Body Inline Script\n" ; </script> <!--Body外部延迟脚本 External Body Script defer--> <script type="text/javascript" defer = "defer" src = "external_body_defer.js"></script> <!--Body外部脚本 External Body Script--> <script type="text/javascript" src = "external_body.js"></script> <script type="text/javascript"> document.getElementsByTagName("button")[0].onclick = function(){console.log(result);} ; </script> </body> </html>
运行结果以下:
从上面能够看出几个问题:
首先,IE9如下不支持DOMContentLoaded(后面会说明这个状况)
其次,验证了上面说的Chrome和Firefox行内脚本不支持defer属性
最后,defer确实达到了延迟执行的目的,没有阻塞后面脚本的加载和执行。可是耗时的操做仍是会阻塞DOMContentLoaded事件,而大多数状况下你们都会把页面初始化的脚本附加在DOMContentLoaded事件上,因此defer方法仍是不能很好解决这个问题。java
2.Script DOM
这是最经常使用也是如今广泛的解决方法。它只须要简单几句话就能够实现脚本的异步加载,而且全部浏览器都支持这个方法。可是在每一个浏览器中,执行仍是略有不一样。看下面这个例子:segmentfault
<html> <meta charset="utf-8"> <head> <script type="text/javascript"> var result = "\n" ; var head = document.getElementsByTagName("head")[0] ; //DOMContentLoaded if(window.addEventListener){ document.addEventListener("DOMContentLoaded",function(){ alert("DOMContentLoaded") ; result += "DOMContentLoaded\n" ; }) ; }else{ document.attachEvent("onDOMContentLoaded",function(){ alert("DOMContentLoaded") ; result += "DOMContentLoaded\n" ; }) ; } window.onload = function(){ result += "window loaded\n"; } ; </script> <!--头部外部延迟脚本 External Head Script defer--> <script type="text/javascript" src = "external_head_defer.js" defer="defer"></script> <!--头部行内脚本 External Head Script--> <script type="text/javascript" src = "external_head.js"></script> </head> <body> <button>SHOW</button> <script type="text/javascript"> document.getElementsByTagName("button")[0].onclick = function(){console.log(result);} ; </script> <script type="text/javascript"> result += "start\n" ; var head = document.getElementsByTagName("head")[0] ; var script8 = document.createElement("script") ; script8.type = "text/javascript" ; script8.onload = function(){alert("done");} ; script8.readystatechange = function(){ if(script8.readyState == "loaded" || script8.readyState == "complete"){ alert("done") ; } } ; //Body Dynamic Script script8.src = "dynamic_body.js" ; head.appendChild(script8) ; result += "end\n" ; </script> </body> </html>
运行结果以下:
下面这张图是在ScriptDom脚本后面加入一个耗时的脚本,使得这个脚本执行完成后,保证ScriptDOM的脚本处于可执行状态:浏览器
<script type="text/javascript"> function doSomething(length){ var start = new Date().getTime() ; while((new Date().getTime() - start) < 1000 * length){} } doSomething(3) ; </script>
结果以下:
运行结果同时也说明了几个问题:
首先,ScriptDOM不会阻塞后续脚本的执行,根据start和end 的位置能够很容易看出。
其次,在第二张图的状况下,ScriptDOM和defer同时均可以执行,在不一样浏览器中它们的优先级的不同的。在Firfox和Chrome中,ScriptDOM的优先级比defer低,而在IE中状况则相反。
最后,经过两种状况的对比发现,在Chrome中ScriptDOM不会阻塞DOMContentLoaded事件可是会阻塞onload事件;在Firefox中ScriptDOM既会阻塞DOMContentLoaded事件也会阻塞onload事件;而在IE中,状况则要根据代码执行状况来决定。若是在DOMContentLoaded事件或者onload事件触发以前,ScriptDOM代码处于可执行状态,那么就会阻塞两个事件;若是在DOMContentLoaded事件或者onload事件触发以前,ScriptDOM代码处于不可执行状态,那么就不会阻塞两个事件。总结的来讲就是在Chrome和IE中DOMContentLoaded事件不须要等待ScriptDOM执行,而在Firefox中须要等待ScriptDOM执行。服务器
经过上面两种方法的对比发现,defer和ScriptDOM都不会阻塞后续脚本的执行。可是相对来讲,ScriptDOM在使用上更加灵活并且并不老是阻塞DOMContentLoaded事件,而且ScriptDOM的使用场景主要是在按需加载和模块加载器上,而通常使用这些技术的时候,页面已经处于加载完成的状态,因此对于性能不会有影响。
app
上面说到DOMContentLoaded事件,DOMcontentLoaded是现代浏览器才支持的一个事件,万恶的IE从IE9开始才支持这个事件。那么在什么状况下才会触发DOMContentLoaded事件呢?DOMContentLoaded会在浏览器接收到服务器传过来的HTML文档,整个页面DOM结构加载完成而且全部行内脚本和外部脚本执行完成后触发 (经过上面异步脚本的例子能够看出,ScriptDOM异步加载脚本不会阻塞DOMContentLoaded,或者说DOMContentLoaded不须要等待ScriptDOM执行就能够出发) ,它跟onload事件的区别是,DOMContentLoaded事件不须要等待图片,ifram和样式表等资源加载完成就会触发,而onload事件须要等待整个页面都加载完成包括各类资源才会触发。因此对于咱们来讲DOMContentLoaded是一个更有用的事件,由于只要DOM结构加载完成,咱们就能够经过Javasscript来操做页面上的DOM节点。
可是上面关于DOMContentLoaded事件触发条件的定义只是官方文档的说法,具体状况并不老是这样。
有时样式表的加载会阻塞脚本的执行从而阻塞DOMContentLoaded事件,这种状况通常出如今样式表后面跟着脚本。也就是说若是把脚本放在样式表后面,那么脚本就必须等到样式表加载完成才能开始执行,这样就会阻塞页面的DOMContentLoaded事件。可是这样作也是有道理的,由于有时候咱们的脚本会处理DOM样式方面的东西。
这种阻塞状况在不一样浏览器上表现也会不同。在IE和Firefox中,无论样式表后面跟着是行内脚本仍是外部脚本,都会发生阻塞。在Chrome中,只有外部脚本才会发生阻塞。
因为IE在IE9如下不支持DOMContentLoaded事件,因此咱们须要用一些Hack技术来实现这个功能。分两种状况来实现:
1.网页不嵌套在iframe中
在IE中咱们能够经过一个方式来判断DOM是否加载完成,就是doScroll方法。若是DOM加载完成,那么咱们就能够调用document的doScroll方法,不然就会抛出异常。咱们能够利用这个特性不断轮询来作Hack。异步
function bindReady(handle){ //判断是否在iframe中 try{ var isFrame = window.frameElement != null ; }catch(e){} if(document.documentElement.doScroll && !isFrame){ //轮询是否能够调用doScroll方法 function tryScroll(){ try{ document.documentElement.doScroll("left"); handle() ; }catch(e){ setTimeout(tryScroll,10) ; } } tryScroll() ; } }
2.网页嵌套在iframe中
若是网页嵌套在iframe中,那么是没法经过doScroll的方法来Hack实现DOMContentLoaded的。咱们能够经过另一种方式来实现---readystatechange,代码以下:函数
function bindReady(handle){ document.onreadystatechange = function(){ if(document.readyState === "complete" || document.readyState === "loaded"){ handle() ; } } }
结合上面的讨论,咱们能够得出一个通用的bindReady方法。性能
//绑定DOMContentLoaded事件,支持绑定多个处理函数 var handleList = [] ; function onReady(handle){ //按顺序执行处理函数 var doHandles = function(){ var length = handleList.length ; for(var i = 0 ; i < length ; i ++){ handleList[i]() ; } } if(handleList.length == 0){ //在尚未处理函数时,把doHandles注册到ready上,这样后面加入的处理函数就能够一并执行 bindReady(doHandles) ; } //把处理函数加入到函数列表中 handleList.push(handle) ; } function bindReady(handle){ var called = false ; var ready = function(){ //防止重复调用 if(!called){ called = true ; handle() ; } } if(document.addEventListener){ //支持DOMcontentLoaded document.addEventListener("DOMContentLoaded",ready,false); }else if(document.attachEvent){ //IE try{ var isFrame = window.frameElement != null ; }catch(e){} //网页不在iframe中 if(document.documentElement.doScroll && !isFrame){ function tryScroll(){ try{ document.documentElement.doScroll("left") ; ready() ; }catch(e){ setTimeout(tryScroll,10) ; } } tryScroll() ; }else{ //网页在iframe中 document.onreadystatechange = function(){ if(document.readyState === "complete" || document.readyState === "loaded"){ ready() ; } } } } //老式浏览器不支持上面两种事件 if(window.addEventListener){ window.addEventListener("load",ready,false) ; }else if(window.attachEvent){ window.attachEvent("onload",ready) ; }else{ //容许绑定多个处理函数 var fn = window.onload ; window.onload = function(){ fn && fn() ; ready() ; } } }
说了这么多,虽然经过脚本后置和异步加载能够下降脚本加载对页面的影响,可是就算是实现了异步加载,可是因为浏览器的脚本解析的单线程的,因此脚本执行的时候仍然会阻塞整个页面(固然除了使用Web Worker),这时候用户是没法完成正常交互的,因此要想真正完全的优化页面加载,还须要从代码的优化开始。从下一篇开始,我会分享关于这方面的学习。