JavaScript异步加载浅析
前言
关于JavaScript脚本加载的问题,相信大家碰到很多。主要在几个点——
1>同步脚本和异步脚本带来的文件加载、文件依赖及执行顺序问题
2>同步脚本和异步脚本带来的性能优化问题
深入理解脚本加载相关的方方面面问题,不仅利于解决实际问题,更加利于对性能优化的把握并执行。
先看随便一个script标签代码——
<scriptsrc="js/myApp.js"></script>
如果放在<head>上面,会阻塞所有页面渲染工作,使得用户在脚本加载完毕并执行完毕之前一直处于“白屏死机”状态。而<body>末尾的打脚本只会让用户看到毫无生命力的静态页面,原本应该进行客户端渲染的地方却散布着不起作用的控件和空空如也的方框。拿一个测试用例——
<!DOCTYPEhtml> <html> <headlang="en"> <metacharset="UTF-8"> <title>异步加载script</title> <scriptsrc="js/test.js"></script> </head> <body> <div>我是内容</div> <imgsrc="img/test.jpg"> </body> </html>
其中,test.js中的内容——
alert('我是head里面的脚本代码,执行这里的js之后,才开始进行body的内容渲染!');
我们会看到,alert是一个暂停点,此时,页面是空白的。但是要注意,此时整个页面已经加载完毕,如果body中包含某些src属性的标签(如上面的img标签),此时浏览器已经开始加载相关内容了。总之要注意——js引擎和渲染引擎的工作时机是互斥的(一些书上叫它为UI线程)。
因此,我们需要——那些负责让页面更好看、更好用的脚本应该立即加载,而那些可以待会儿再加载的脚本稍后再加载。
一、脚本延迟执行
现在越来越流行把脚本放在页面<body>标签的尾部。这样,一方面用户可以更快地看到页面,另一方面脚本可以直接操作已经加载完成的dom元素。对于大多数脚本而言,这次“搬家”是个巨大的进步。该页面模型如下——
<!DOCTYPEhtml> <html> <headlang="en"> <!--metadataandscriptsheetsgohere--> <scriptsrc="headScript.js"></script> </head> <body> <!--contentgoeshere--> <scriptsrc="bodyScript.js"></script> </body> </html>
这确实大大加快了页面的渲染时间,但是注意一点,这可能让用户有机会在加载bodyScript之前与页面交互。源于浏览器在加载完整个文档之前无法加载这些脚本,这对那些通过慢速连接传送的大型文档来说会是一大瓶颈。
理想情况下,脚本的加载应该与文档的加载同时进行,并且不影响DOM的渲染。这样,一旦文档就绪就可以运行脚本,因为已经按照<script>标签的次序加载了相应脚本。
我们使用defer便能够完成这样的需求,即——
<scriptsrc="deferredScript.js"></script>
添加defer属性相当于告诉浏览器:请马上开始加载这个脚本吧,但是,请等到文档就绪且此前所有具有defer属性的脚本都结束运行之后再运行它。
这样,在head标签里放入延迟脚本,技能带来脚本置于body标签时的所有好处,又能让大文档的加载速度大幅提升。此时的页面模式便是——
<!DOCTYPEhtml> <html> <headlang="en"> <!--metadataandscriptsheetsgohere--> <scriptsrc="headScript.js"></script> <scriptsrc="deferredScript.js"defer></script> </head> <body> <!--contentgoeshere--> </body> </html>
但是并非所有的浏览器都支持defer(对于一些modern浏览器,如果声明defer,其内部脚本将不会执行document.write及DOM渲染操作。IE4+均支持defer属性)。这意味着,如果想确保自己的延迟脚本能在文档加载后运行,就必须将所有延迟脚本的代码都封装在诸如jQuery之$(document).ready之类的结构中。这是值得的,因为差不多97%的访客都能享受到并行加载的好处,同时另外3%的访客仍然能使用功能完整的JavaScript。
二、脚本的完全并行化
让脚本的加载及执行再快一步,我不想等到defer脚本一个接着一个运行(defer让我们想到一种静静等待文档加载的有序排队场景),更不想等到文档就绪之后才运行这些脚本,我想要尽快加载并且尽快运行这些脚本。这里也就想到了HTML5的async属性,但是要注意,它是一种混乱的无政府状态。
例如,我们加载两个完全不相干的第三方脚本,页面没有它们也运行得很好,而且也不在乎它们谁先运行谁后运行。因此,对这些第三方脚本使用async属性,相当于一分钱没花就提升了它们的运行速度。
async属性是HTML5新增的。作用和defer类似,即允许在下载脚本的同时进行DOM的渲染。但是它将在下载后尽快执行(即JS引擎空闲了立马执行),不能保证脚本会按顺序执行。它们将在onload事件之前完成。
Firefox3.6、Opera10.5、IE9和最新的Chrome和Safari都支持async属性。可以同时使用async和defer,这样IE4之后的所有IE都支持异步加载,但是要注意,async会覆盖掉defer。
那么此时的页面模型如下——
<!DOCTYPEhtml> <html> <headlang="en"> <!--metadataandscriptsheetsgohere--> <scriptsrc="headScript.js"></script> <scriptsrc="deferredScript.js"defer></script> </head> <body> <!--contentgoeshere--> <scriptsrc="asyncScript1.js"asyncdefer></script> <scriptsrc="asyncScript2.js"asyncdefer></script> </body> </html>
要注意这里的执行顺序——各个脚本文件加载,接着执行headScript.js,紧接着在DOM渲染的同时会在后台加载defferedScript.js。接着在DOM渲染结束时将运行defferedScript.js和那两个异步脚本,要注意对于支持async属性的浏览器而言,这两个脚本将做无序运行。
三、可编程的脚本加载
尽管上面两个脚本属性的功能非常吸引人,但是由于兼容性的问题,应用并不是很广泛。故此,我们更多使用脚本加载其他脚本。例如,我们只想给那些满足一定条件的用户加载某个脚本,也就是经常提到的“懒加载”。
在浏览器API层面,有两种合理的方法来抓取并运行服务器脚本——
1>生成ajax请求并用eval函数处理响应
2>向DOM插入<script>标签
后一种方式更好,因为浏览器会替我们操心生成HTTP请求这样的事。再者,eval也有一些实际问题:泄露作用域,调试搞得一团糟,而且还可能降低性能。因此,想要加载名为feture.js的脚本,我们应该使用类似下面的代码:
varhead=document.getElementsByTagName('head')[0]; varscript=document.createElement('script'); script.src='feature.js'; head.appendChild(script);
当然,我们要处理回调监听,HTML5规范定义了一个可以绑定回调的onload属性。
script.onload=function(){ console.log('scriptloaded...'); }
不过,IE8及更老的版本并不支持onload,它们支持的是onreadystatechange。而且,对于错误处理仍然千奇百怪。在这里,可以多参考一些流行的校本加载库,如labjs、yepnope、requirejs等。
如下,自己封装了一个简易loadjs文件——
varloadJS=function(url,callback){ varhead=document.getElementsByTagName('head')[0]; varscript=document.createElement('script'); script.src=url; script.type="text/javascript"; head.appendChild(script);
//script标签,IE下有onreadystatechange事件,w3c标准有onload事件 //IE9+也支持W3C标准的onload varua=navigator.userAgent, ua_version; //IE6/7/8 if(/MSIE([^;]+)/.test(ua)){ ua_version=parseFloat(RegExp["$1"],10); if(ua_version<=8){ script.onreadystatechange=function(){ if(this.readyState=="loaded"){ callback(); } } }else{ script.onload=function(){ callback(); }; } }else{ script.onload=function(){ callback(); }; } };