[TOC]
# js的并行加載與順序執(zhí)行
javaScript文件(下面簡(jiǎn)稱腳本文件)需要被HTML文件引用才能在瀏覽器中運(yùn)行。在HTML文件中可以通過(guò)不同的方式來(lái)引用腳本文件,我們需要關(guān)注的是,這些方式的具體實(shí)現(xiàn)和這些方式可能會(huì)帶來(lái)的性能問(wèn)題。
當(dāng)瀏覽器遇到(內(nèi)嵌)`<script>`標(biāo)簽時(shí),當(dāng)前瀏覽器無(wú)從獲知javaScript是否會(huì)修改頁(yè)面內(nèi)容。因此,這時(shí)瀏覽器會(huì)停止處理頁(yè)面,先執(zhí)行javaScript代碼,然后再繼續(xù)解析和渲染頁(yè)面。同樣的情況也發(fā)生在使用 `src` 屬性加在javaScript的過(guò)程中(即外鏈 javaScript),瀏覽器必須先花時(shí)間下載外鏈文件中的代碼,然后解析并執(zhí)行它。在這個(gè)過(guò)程中,頁(yè)面渲染和用戶交互完全被阻塞了。
也就是說(shuō):每當(dāng)瀏覽器解析到`<script>`標(biāo)簽(無(wú)論內(nèi)嵌還是外鏈)時(shí),瀏覽器會(huì)(一根筋地)優(yōu)先下載、解析并執(zhí)行該標(biāo)簽中的javaScript代碼,而阻塞了其后所有頁(yè)面內(nèi)容的下載和渲染。
## 五種引用腳本的方式:
1. Script DOM Element。 動(dòng)態(tài)插入`<script>`,不會(huì)阻塞,但無(wú)法保持執(zhí)行順序。但唯有Firefox可以保持執(zhí)行順序,但也差點(diǎn)在Firefox 4 nightly的版本中去掉這個(gè)特性。
這種技術(shù)的重點(diǎn)在于:
> 無(wú)論在何時(shí)啟動(dòng)下載,文件的下載和執(zhí)行過(guò)程不會(huì)阻塞頁(yè)面其他進(jìn)程(包括腳本加載)。
然而這種方法也是有缺陷的。這種方法加載的腳本會(huì)在下載完成后立即執(zhí)行,那么意味著多個(gè)腳本之間的運(yùn)行順序是無(wú)法保證的(除了Firefox和Opera)。當(dāng)某個(gè)腳本對(duì)另一個(gè)腳本有依賴關(guān)系時(shí),就很可能發(fā)生錯(cuò)誤了。比如,寫(xiě)一個(gè)jQuery代碼,需要引入jQuery庫(kù),然而你寫(xiě)的jQuery代碼文件很可能會(huì)先完成下載并立即執(zhí)行,這時(shí)瀏覽器會(huì)報(bào)錯(cuò)——‘`jQuery未定義`’之類(lèi)的,因?yàn)榇藭r(shí)jQuery庫(kù)還未下載完成。于是做出以下改進(jìn):
~~~
function loadScript(url,callback){
var script=document.createElement(‘script’);
script.type=”text/javaScript”;
if(script.readyState){//IE
script.onreadystatechange=function(){
if(script.readyState==”loaded”||script.readyState==”complete”){
script.onreadystatechange=null; callback(); } };
}else{//其他瀏覽器
script.onload=function(){ callback(); }; } script.src=url;
document.getElementsByTagName(‘head’)[0].appendChild(script);
}
~~~
上述代碼改進(jìn)的地方就是增加了一個(gè)回調(diào)函數(shù),該函數(shù)會(huì)在相應(yīng)腳本文件加載完成后被調(diào)用。這樣便可以實(shí)現(xiàn)順序加載了,寫(xiě)法如下(假設(shè)file2依賴file1,file1和file3相互獨(dú)立):
`loadScript(‘file1.js’,function(){ loadScript(‘file2.js’,function(){}); }); loadScript(‘file3.js’,function(){}); `
file2會(huì)在file1加載完后才開(kāi)始加載,保證了在file2執(zhí)行前file1已經(jīng)準(zhǔn)備妥當(dāng)。而file1和file3是并行下載的,互不影響。 雖然loadScript函數(shù)已經(jīng)足夠好,但還是有些不盡人意的地方——通過(guò)分析這段代碼,我們知道,loadScript函數(shù)中的順序加載是以腳本的阻塞加載來(lái)實(shí)現(xiàn)的(正如上述紅字部分指出的那樣)。而我們真正想實(shí)現(xiàn)的是——腳本同步下載并按相應(yīng)順序執(zhí)行,即并行加載并順序執(zhí)行。
2. HTML5 async 非阻塞,加載完后立即執(zhí)行,不保證順序。這個(gè)屬性不管有沒(méi)有值、值為true或false,都是等同的效果(由于Kyle的推進(jìn),不能保證執(zhí)行順序與其值無(wú)關(guān)了)。
Google Analytics的新版嵌入代碼就結(jié)合使用了上面兩個(gè)方案,如:
~~~
var ga = document.createElement('script');
ga.type = 'text/javascript';
ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www')
+ '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(ga, s);
~~~
3. IE partsandspares.co.za defer屬性。不阻塞,可以保證順序,在DOM加載完成后執(zhí)行(在`DOMContentLoaded`之前)。
4. `<script>`的type屬性設(shè)為”script/cache” 非標(biāo)準(zhǔn)的type屬性,使js文件只會(huì)被加載而不會(huì)執(zhí)行。需要執(zhí)行時(shí),創(chuàng)建一個(gè)type屬性為”text/JavaScript”的正常`<script>`元素,src設(shè)為前面已經(jīng)加載的js地址即可,執(zhí)行順序開(kāi)發(fā)者可控(執(zhí)行時(shí)機(jī)也完全可控)。類(lèi)似的方式也有通過(guò)`<img>`來(lái)做預(yù)加載的。
5. `document.write`。文檔流關(guān)閉后執(zhí)行會(huì)清空整個(gè)頁(yè)面。
6. `XHR` 并行加載,執(zhí)行順序可控,但有同域限制。
## 基本需求:
Steve Souders 和 Nicholas C. Zakas 一起總結(jié)了下,認(rèn)為js加載方案必須解決以下問(wèn)題:
* 支持特性檢測(cè)
* 不會(huì)重復(fù)加載
* 支持并行加載
## 解決方案:
head.js
LABjs
LABjs庫(kù)能幫我們真正地實(shí)現(xiàn)“并行加載與順序執(zhí)行”,推薦寫(xiě)法如下:
~~~
<script src="LAB.js"></script>
<script type="text/javaScript">
$LAB
.script("script1.js").wait()
.script("script2-a.js")
.script("script2-b.js")
.wait(function(){
initScript1();
initScript2();
})
.script("script3.js")
.wait(function(){
initScript3();
});
</script>
~~~
requireJS
~~~
<script src="require.js"></script>
<script type="text/javaScript">
require([
"script1.js",
"script2-a.js",
"script2-b.js",
"script3.js"
],
function(){
initScript1();
initScript2();
initScript3();
}
);
</script>
~~~
## 附錄知識(shí)
### 預(yù)編譯期與執(zhí)行期
JS是按照代碼塊來(lái)進(jìn)行編譯和執(zhí)行的,**代碼塊間相互獨(dú)立,但變量和方法共享**。什么意思呢? 舉個(gè)例子,你就明白了:
~~~
<script type="text/javascript">
alert(str);//因?yàn)闆](méi)有定義str,所以瀏覽器會(huì)出錯(cuò),下面的不能運(yùn)行
alert("我是代碼塊一");//沒(méi)有運(yùn)行到這里
var test = "我是代碼塊一變量";
</script>
<script type="text/javascript">
alert("我是代碼塊二"); //這里有運(yùn)行到
alert(test); //彈出"我是代碼塊一變量"
</script>
~~~
上面的代碼中代碼塊一中運(yùn)行報(bào)錯(cuò),但不影響代碼塊二的執(zhí)行,這就是代碼塊間的獨(dú)立性,而代碼塊二中能調(diào)用到代碼一中的變量,則是塊間共享性。
JS的解析過(guò)程分為兩個(gè)階段:**預(yù)編譯期(預(yù)處理)與執(zhí)行期**。
預(yù)編譯期 JS會(huì)對(duì)本代碼塊(兩個(gè)script塊互不影響)中的所有var聲明的變量和函數(shù)進(jìn)行處理(類(lèi)似與C語(yǔ)言的編譯)
此時(shí)處理函數(shù)的只是聲明式函數(shù),而且變量也只是進(jìn)行了聲明但未進(jìn)行初始化以及賦值。
執(zhí)行期 會(huì)按照代碼塊的順序逐行執(zhí)行
`代碼塊`:
~~~
<script type="text/javascript">
alert("first");
function Fn(){
alert("third");
}
</script>
<script type="text/javascript">
alert("second");
</script>
~~~
可以總結(jié)出js執(zhí)行的順序:
~~~
step 1. 讀入第一個(gè)代碼塊。
step 2. 做語(yǔ)法分析,有錯(cuò)則報(bào)語(yǔ)法錯(cuò)誤(比如括號(hào)不匹配等),并跳轉(zhuǎn)到 step 5。
step 3. 對(duì) var 變量和 function 定義做“預(yù)編譯處理”(永遠(yuǎn)不會(huì)報(bào)錯(cuò)的,因?yàn)橹唤馕稣_的聲明)。
step 4. 執(zhí)行代碼段,有錯(cuò)則報(bào)錯(cuò)(比如變量未定義)。
step 5. 如果還有下一個(gè)代碼段,則讀入下一個(gè)代碼段,重復(fù) step 2。
step 6. 結(jié)束。
~~~
`加載順序測(cè)試代碼:`
根據(jù)html文檔流的執(zhí)行順序,
需要在頁(yè)面元素**渲染前**執(zhí)行的js代碼應(yīng)該放在`<body>`前面的`<script>`代碼塊中,
需要在頁(yè)面**加載完成后**執(zhí)行的js放在`</body>`元素后邊,
`body`的**onload事件是最后執(zhí)行**的。

- 步入JavaScript的世界
- 二進(jìn)制運(yùn)算
- JavaScript 的版本是怎么回事?
- JavaScript和DOM的產(chǎn)生與發(fā)展
- DOM事件處理
- js的并行加載與順序執(zhí)行
- 正則表達(dá)式
- 當(dāng)遇上this時(shí)
- Javascript中apply、call、bind
- JavaScript的編譯過(guò)程與運(yùn)行機(jī)制
- 執(zhí)行上下文(Execution Context)
- javascript 作用域
- 分組中的函數(shù)表達(dá)式
- JS之constructor屬性
- Javascript 按位取反運(yùn)算符 (~)
- EvenLoop 事件循環(huán)
- 異步編程
- JavaScript的九個(gè)思維導(dǎo)圖
- JavaScript奇淫技巧
- JavaScript:shim和polyfill
- ===值得關(guān)注的庫(kù)===
- ==文章==
- JavaScript框架
- Angular 1.x
- 啟動(dòng)引導(dǎo)過(guò)程
- $scope作用域
- $q與promise
- ngRoute 和 ui-router
- 雙向數(shù)據(jù)綁定
- 規(guī)范和性能優(yōu)化
- 自定義指令
- Angular 事件
- lodash
- Test
