[TOC]
# js的并行加載與順序執行
javaScript文件(下面簡稱腳本文件)需要被HTML文件引用才能在瀏覽器中運行。在HTML文件中可以通過不同的方式來引用腳本文件,我們需要關注的是,這些方式的具體實現和這些方式可能會帶來的性能問題。
當瀏覽器遇到(內嵌)`<script>`標簽時,當前瀏覽器無從獲知javaScript是否會修改頁面內容。因此,這時瀏覽器會停止處理頁面,先執行javaScript代碼,然后再繼續解析和渲染頁面。同樣的情況也發生在使用 `src` 屬性加在javaScript的過程中(即外鏈 javaScript),瀏覽器必須先花時間下載外鏈文件中的代碼,然后解析并執行它。在這個過程中,頁面渲染和用戶交互完全被阻塞了。
也就是說:每當瀏覽器解析到`<script>`標簽(無論內嵌還是外鏈)時,瀏覽器會(一根筋地)優先下載、解析并執行該標簽中的javaScript代碼,而阻塞了其后所有頁面內容的下載和渲染。
## 五種引用腳本的方式:
1. Script DOM Element。 動態插入`<script>`,不會阻塞,但無法保持執行順序。但唯有Firefox可以保持執行順序,但也差點在Firefox 4 nightly的版本中去掉這個特性。
這種技術的重點在于:
> 無論在何時啟動下載,文件的下載和執行過程不會阻塞頁面其他進程(包括腳本加載)。
然而這種方法也是有缺陷的。這種方法加載的腳本會在下載完成后立即執行,那么意味著多個腳本之間的運行順序是無法保證的(除了Firefox和Opera)。當某個腳本對另一個腳本有依賴關系時,就很可能發生錯誤了。比如,寫一個jQuery代碼,需要引入jQuery庫,然而你寫的jQuery代碼文件很可能會先完成下載并立即執行,這時瀏覽器會報錯——‘`jQuery未定義`’之類的,因為此時jQuery庫還未下載完成。于是做出以下改進:
~~~
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);
}
~~~
上述代碼改進的地方就是增加了一個回調函數,該函數會在相應腳本文件加載完成后被調用。這樣便可以實現順序加載了,寫法如下(假設file2依賴file1,file1和file3相互獨立):
`loadScript(‘file1.js’,function(){ loadScript(‘file2.js’,function(){}); }); loadScript(‘file3.js’,function(){}); `
file2會在file1加載完后才開始加載,保證了在file2執行前file1已經準備妥當。而file1和file3是并行下載的,互不影響。 雖然loadScript函數已經足夠好,但還是有些不盡人意的地方——通過分析這段代碼,我們知道,loadScript函數中的順序加載是以腳本的阻塞加載來實現的(正如上述紅字部分指出的那樣)。而我們真正想實現的是——腳本同步下載并按相應順序執行,即并行加載并順序執行。
2. HTML5 async 非阻塞,加載完后立即執行,不保證順序。這個屬性不管有沒有值、值為true或false,都是等同的效果(由于Kyle的推進,不能保證執行順序與其值無關了)。
Google Analytics的新版嵌入代碼就結合使用了上面兩個方案,如:
~~~
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加載完成后執行(在`DOMContentLoaded`之前)。
4. `<script>`的type屬性設為”script/cache” 非標準的type屬性,使js文件只會被加載而不會執行。需要執行時,創建一個type屬性為”text/JavaScript”的正常`<script>`元素,src設為前面已經加載的js地址即可,執行順序開發者可控(執行時機也完全可控)。類似的方式也有通過`<img>`來做預加載的。
5. `document.write`。文檔流關閉后執行會清空整個頁面。
6. `XHR` 并行加載,執行順序可控,但有同域限制。
## 基本需求:
Steve Souders 和 Nicholas C. Zakas 一起總結了下,認為js加載方案必須解決以下問題:
* 支持特性檢測
* 不會重復加載
* 支持并行加載
## 解決方案:
head.js
LABjs
LABjs庫能幫我們真正地實現“并行加載與順序執行”,推薦寫法如下:
~~~
<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>
~~~
## 附錄知識
### 預編譯期與執行期
JS是按照代碼塊來進行編譯和執行的,**代碼塊間相互獨立,但變量和方法共享**。什么意思呢? 舉個例子,你就明白了:
~~~
<script type="text/javascript">
alert(str);//因為沒有定義str,所以瀏覽器會出錯,下面的不能運行
alert("我是代碼塊一");//沒有運行到這里
var test = "我是代碼塊一變量";
</script>
<script type="text/javascript">
alert("我是代碼塊二"); //這里有運行到
alert(test); //彈出"我是代碼塊一變量"
</script>
~~~
上面的代碼中代碼塊一中運行報錯,但不影響代碼塊二的執行,這就是代碼塊間的獨立性,而代碼塊二中能調用到代碼一中的變量,則是塊間共享性。
JS的解析過程分為兩個階段:**預編譯期(預處理)與執行期**。
預編譯期 JS會對本代碼塊(兩個script塊互不影響)中的所有var聲明的變量和函數進行處理(類似與C語言的編譯)
此時處理函數的只是聲明式函數,而且變量也只是進行了聲明但未進行初始化以及賦值。
執行期 會按照代碼塊的順序逐行執行
`代碼塊`:
~~~
<script type="text/javascript">
alert("first");
function Fn(){
alert("third");
}
</script>
<script type="text/javascript">
alert("second");
</script>
~~~
可以總結出js執行的順序:
~~~
step 1. 讀入第一個代碼塊。
step 2. 做語法分析,有錯則報語法錯誤(比如括號不匹配等),并跳轉到 step 5。
step 3. 對 var 變量和 function 定義做“預編譯處理”(永遠不會報錯的,因為只解析正確的聲明)。
step 4. 執行代碼段,有錯則報錯(比如變量未定義)。
step 5. 如果還有下一個代碼段,則讀入下一個代碼段,重復 step 2。
step 6. 結束。
~~~
`加載順序測試代碼:`
根據html文檔流的執行順序,
需要在頁面元素**渲染前**執行的js代碼應該放在`<body>`前面的`<script>`代碼塊中,
需要在頁面**加載完成后**執行的js放在`</body>`元素后邊,
`body`的**onload事件是最后執行**的。

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