[TOC]
瀏覽器通過內置的JavaScript引擎,讀取網頁中的代碼,對其處理后運行。
## JavaScript代碼嵌入網頁的方法
在網頁中嵌入JavaScript代碼有多種方法。
### 直接添加代碼塊
通過script標簽,可以直接將JavaScript代碼嵌入網頁。
~~~
<script>
// some JavaScript code
</script>
~~~
### 加載外部腳本
script標簽也可以指定加載外部的腳本文件。
~~~
<script src="example.js"></script>
~~~
如果腳本文件使用了非英語字符,還應該注明編碼。
~~~
<script charset="utf-8" src="example.js"></script>
~~~
加載外部腳本和直接添加代碼塊,這兩種方法不能混用。下面代碼的console.log語句直接被忽略。
~~~
<script charset="utf-8" src="example.js">
console.log('Hello World!');
</script>
~~~
### 行內代碼
除了上面兩種方法,HTML語言允許在某些元素的事件屬性和a元素的href屬性中,直接寫入JavaScript。
~~~
<div onclick="alert('Hello')"></div>
<a href="javascript:alert('Hello')"></a>
~~~
這種寫法將HTML代碼與JavaScript代碼混寫在一起,非常不利于代碼管理,不建議使用。
## 外部腳本的加載
### 網頁底部加載
正常的網頁加載流程是這樣的。
1. 瀏覽器一邊下載HTML網頁,一邊開始解析
2. 解析過程中,發現script標簽
3. 暫停解析,下載script標簽中的外部腳本
4. 下載完成,執行腳本
5. 恢復往下解析HTML網頁
也就是說,加載外部腳本時,瀏覽器會暫停頁面渲染,等待腳本下載并執行完成后,再繼續渲染。如果加載時間很長(比如一直無法完成下載),就會造成網頁長時間失去響應,瀏覽器就會呈現“假死”狀態,失去響應,這被稱為“阻塞效應”。這樣設計是因為JavaScript代碼可能會修改頁面,所以必須等它執行完才能繼續渲染。為了避免這種情況,較好的做法是將script標簽都放在頁面底部,而不是頭部。當然,如果某些腳本代碼非常重要,一定要放在頁面頭部的話,最好直接將代碼嵌入頁面,而不是連接外部腳本文件,這樣能縮短加載時間。
將腳本文件都放在網頁尾部加載,還有一個好處。在DOM結構生成之前就調用DOM,JavaScript會報錯,如果腳本都在網頁尾部加載,就不存在這個問題,因為這時DOM肯定已經生成了。
~~~
<head>
<script>
console.log(document.body.innerHTML);
</script>
</head>
~~~
上面代碼執行時會報錯,因為此時body元素還未生成。
一種解決方法是設定DOMContentLoaded事件的回調函數。
~~~
<head>
<script>
document.addEventListener("DOMContentLoaded", function(event) {
console.log(document.body.innerHTML);
});
</script>
</head>
~~~
另一種解決方法是,使用script標簽的onload屬性。當script標簽指定的外部腳本文件下載和解析完成,會觸發一個load事件,可以為這個事件指定回調函數。
~~~
<script src="jquery.min.js" onload="console.log(document.body.innerHTML)">
</script>
~~~
但是,如果將腳本放在頁面底部,就可以完全按照正常的方式寫,上面兩種方式都不需要。
~~~
<body>
<!-- 其他代碼 -->
<script>
console.log(document.body.innerHTML);
</script>
</body>
~~~
### 多個腳本的加載
如果有多個script標簽,比如下面這樣。
~~~
<script src="1.js"></script>
<script src="2.js"></script>
~~~
瀏覽器會同時平行下載1.js和2.js,但是執行時會保證先執行1.js,然后再執行2.js,即使后者先下載完成,也是如此。也就是說,腳本的執行順序由它們在頁面中的出現順序決定,這是為了保證腳本之間的依賴關系不受到破壞。
當然,加載這兩個腳本都會產生“阻塞效應”,必須等到它們都加載完成,瀏覽器才會繼續頁面渲染。
此外,對于來自同一個域名的資源,比如腳本文件、樣式表文件、圖片文件等,瀏覽器一般最多同時下載六個。如果是來自不同域名的資源,就沒有這個限制。所以,通常把靜態文件放在不同的域名之下,以加快下載速度。
### defer屬性
為了解決腳本文件下載阻塞網頁渲染的問題,一個方法是加入defer屬性。
~~~
<script src="1.js" defer></script>
<script src="2.js" defer></script>
~~~
defer屬性的運行過程是這樣的。
1. 瀏覽器開始解析HTML網頁
2. 解析過程中,發現帶有defer屬性的script標簽
3. 瀏覽器繼續往下解析HTML網頁,同時并行下載script標簽中的外部腳本
4. 瀏覽器完成解析HTML網頁,此時再執行下載的腳本
有了defer屬性,瀏覽器下載腳本文件的時候,不會阻塞頁面渲染。下載的腳本文件在DOMContentLoaded事件觸發前執行(即剛剛讀取完標簽),而且可以保證執行順序就是它們在頁面上出現的順序。但是,瀏覽器對這個屬性的支持不夠理想,IE(<=9)還有一個bug,無法保證2.js一定在1.js之后執行。如果需要支持老版本的IE,且腳本之間有依賴關系,建議不要使用defer屬性。
對于內置而不是連接外部腳本的script標簽,以及動態生成的script標簽,defer屬性不起作用。
### async屬性
解決“阻塞效應”的另一個方法是加入async屬性。
~~~
<script src="1.js" async></script>
<script src="2.js" async></script>
~~~
async屬性的運行過程是這樣的。
1. 瀏覽器開始解析HTML網頁
2. 解析過程中,發現帶有async屬性的script標簽
3. 瀏覽器繼續往下解析HTML網頁,同時并行下載script標簽中的外部腳本
4. 腳本下載完成,瀏覽器暫停解析HTML網頁,開始執行下載的腳本
5. 腳本執行完畢,瀏覽器恢復解析HTML網頁
async屬性可以保證腳本下載的同時,瀏覽器繼續渲染。需要注意的是,一旦采用這個屬性,就無法保證腳本的執行順序。哪個腳本先下載結束,就先執行那個腳本。使用async屬性的腳本文件中,不應該使用document.write方法。IE 10支持async屬性,低于這個版本的IE都不支持。
defer屬性和async屬性到底應該使用哪一個?一般來說,如果腳本之間沒有依賴關系,就使用async屬性,如果腳本之間有依賴關系,就使用defer屬性。如果同時使用async和defer屬性,后者不起作用,瀏覽器行為由async屬性決定。
### 腳本的動態嵌入
除了用靜態的script標簽,還可以動態嵌入script標簽。
~~~
['1.js', '2.js'].forEach(function(src) {
var script = document.createElement('script');
script.src = src;
document.head.appendChild(script);
});
~~~
這種方法的好處是,動態生成的script標簽不會阻塞頁面渲染,也就不會造成瀏覽器假死。但是問題在于,這種方法無法保證腳本的執行順序,哪個腳本文件先下載完成,就先執行哪個。
如果想避免這個問題,可以設置async屬性為false。
~~~
['1.js', '2.js'].forEach(function(src) {
var script = document.createElement('script');
script.src = src;
script.async = false;
document.head.appendChild(script);
});
~~~
上面的代碼依然不會阻塞頁面渲染,而且可以保證2.js在1.js后面執行。不過需要注意的是,在這段代碼后面加載的腳本文件,會因此都等待2.js執行完成后再執行。
我們可以把上面的寫法,封裝成一個函數。
~~~
(function() {
var script,
scripts = document.getElementsByTagName('script')[0];
function load(url) {
script = document.createElement('script');
script.async = true;
script.src = url;
scripts.parentNode.insertBefore(script, scripts);
}
load('//apis.google.com/js/plusone.js');
load('//platform.twitter.com/widgets.js');
load('//s.thirdpartywidget.com/widget.js');
}());
~~~
此外,動態嵌入還有一個地方需要注意。動態嵌入必須等待CSS文件加載完成后,才會去下載外部腳本文件。靜態加載就不存在這個問題,script標簽指定的外部腳本文件,都是與CSS文件同時并發下載的。
### 加載使用的協議
如果不指定協議,瀏覽器默認采用HTTP協議下載。
~~~
<script src="example.js"></script>
~~~
上面的example.js默認就是采用http協議下載,如果要采用HTTPs協議下載,必需寫明(假定服務器支持)。
~~~
<script src="https://example.js"></script>
~~~
但是有時我們會希望,根據頁面本身的協議來決定加載協議,這時可以采用下面的寫法。
~~~
<script src="//example.js"></script>
~~~
## JavaScript虛擬機
JavaScript是一種解釋型語言,也就是說,它不需要編譯,可以由解釋器實時運行。這樣的好處是運行和修改都比較方便,刷新頁面就可以重新解釋;缺點是每次運行都要調用解釋器,系統開銷較大,運行速度慢于編譯型語言。為了提高運行速度,目前的瀏覽器都將JavaScript進行一定程度的編譯,生成類似字節碼(bytecode)的中間代碼,以提高運行速度。
早期,瀏覽器內部對JavaScript的處理過程如下:
1. 讀取代碼,進行詞法分析(Lexical analysis),將代碼分解成詞元(token)。
2. 對詞元進行語法分析(parsing),將代碼整理成“語法樹”(syntax tree)。
3. 使用“翻譯器”(translator),將代碼轉為字節碼(bytecode)。
4. 使用“字節碼解釋器”(bytecode interpreter),將字節碼轉為機器碼。
逐行解釋將字節碼轉為機器碼,是很低效的。為了提高運行速度,現代瀏覽器改為采用“即時編譯”(Just In Time compiler,縮寫JIT),即字節碼只在運行時編譯,用到哪一行就編譯哪一行,并且把編譯結果緩存(inline cache)。通常,一個程序被經常用到的,只是其中一小部分代碼,有了緩存的編譯結果,整個程序的運行速度就會顯著提升。
不同的瀏覽器有不同的編譯策略。有的瀏覽器只編譯最經常用到的部分,比如循環的部分;有的瀏覽器索性省略了字節碼的翻譯步驟,直接編譯成機器碼,比如chrome瀏覽器的V8引擎。
字節碼不能直接運行,而是運行在一個虛擬機(Virtual Machine)之上,一般也把虛擬機稱為JavaScript引擎。因為JavaScript運行時未必有字節碼,所以JavaScript虛擬機并不完全基于字節碼,而是部分基于源碼,即只要有可能,就通過JIT(just in time)編譯器直接把源碼編譯成機器碼運行,省略字節碼步驟。這一點與其他采用虛擬機(比如Java)的語言不盡相同。這樣做的目的,是為了盡可能地優化代碼、提高性能。下面是目前最常見的一些JavaScript虛擬機:
* [Chakra]([http://en.wikipedia.org/wiki/Chakra_(JScript_engine\))(Microsoft](http://en.wikipedia.org/wiki/Chakra_(JScript_engine%5C))(Microsoft)?Internet Explorer)
* [Nitro/JavaScript Core](http://en.wikipedia.org/wiki/WebKit#JavaScriptCore)?(Safari)
* [Carakan](http://dev.opera.com/articles/view/labs-carakan/)?(Opera)
* [SpiderMonkey](https://developer.mozilla.org/en-US/docs/SpiderMonkey)?(Firefox)
* [V8]([http://en.wikipedia.org/wiki/V8_(JavaScript_engine\)](http://en.wikipedia.org/wiki/V8_(JavaScript_engine%5C))) (Chrome, Chromium)
## 單線程模型
JavaScript采用單線程模型,也就是說,所有的任務都在一個線程里運行。這意味著,一次只能運行一個任務,其他任務都必須在后面排隊等待。
JavaScript之所以采用單線程,而不是多線程,跟歷史有關系。JavaScript從誕生起就是單線程,原因是不想讓瀏覽器變得太復雜,因為多線程需要共享資源、且有可能修改彼此的運行結果,對于一種網頁腳本語言來說,這就太復雜了。比如,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另一個線程刪除了這個節點,這時瀏覽器應該以哪個線程為準?所以,為了避免復雜性,從一誕生,JavaScript就是單線程,這已經成了這門語言的核心特征,將來也不會改變。
為了利用多核CPU的計算能力,HTML5提出Web Worker標準,允許JavaScript腳本創建多個線程,但是子線程完全受主線程控制,且不得操作DOM。所以,這個新標準并沒有改變JavaScript單線程的本質。
單線程模型帶來了一些問題,主要是新的任務被加在隊列的尾部,只有前面的所有任務運行結束,才會輪到它執行。如果有一個任務特別耗時,后面的任務都會停在那里等待,造成瀏覽器失去響應,又稱“假死”。為了避免“假死”,當某個操作在一定時間后仍無法結束,瀏覽器就會跳出提示框,詢問用戶是否要強行停止腳本運行。
如果排隊是因為計算量大,CPU忙不過來,倒也算了,但是很多時候CPU是閑著的,因為IO設備(輸入輸出設備)很慢(比如Ajax操作從網絡讀取數據),不得不等著結果出來,再往下執行。JavaScript語言的設計者意識到,這時CPU完全可以不管IO設備,掛起處于等待中的任務,先運行排在后面的任務。等到IO設備返回了結果,再回過頭,把掛起的任務繼續執行下去。這種機制就是JavaScript內部采用的Event Loop。
## Event Loop
所謂Event Loop,指的是一種內部循環,用來排列和處理事件,以及執行函數。[Wikipedia](http://en.wikipedia.org/wiki/Event_loop)的定義是:“Event Loop是一個程序結構,用于等待和發送消息和事件。(a programming construct that waits for and dispatches events or messages in a program.)”
所有任務可以分成兩種,一種是同步任務(synchronous),另一種是異步任務(asynchronous)。同步任務指的是,在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行后一個任務;異步任務指的是,不進入主線程、而進入“任務隊列”(task queue)的任務,只有“任務隊列”通知主線程,某個異步任務可以執行了,該任務才會進入主線程執行。
以Ajax操作為例,它可以當作同步任務處理,也可以當作異步任務處理,由開發者決定。如果是同步任務,主線程就等著Ajax操作返回結果,再往下執行;如果是異步任務,該任務直接進入“任務隊列”,主線程跳過Ajax操作,直接往下執行,等到Ajax操作有了結果,主線程再執行對應的回調函數。
想要理解Event Loop,就要從程序的運行模式講起。運行以后的程序叫做"進程"(process),一般情況下,一個進程一次只能執行一個任務。如果有很多任務需要執行,不外乎三種解決方法。
1. 排隊。因為一個進程一次只能執行一個任務,只好等前面的任務執行完了,再執行后面的任務。
2. 新建進程。使用fork命令,為每個任務新建一個進程。
3. 新建線程。因為進程太耗費資源,所以如今的程序往往允許一個進程包含多個線程,由線程去完成任務。
如果某個任務很耗時,比如涉及很多I/O(輸入/輸出)操作,那么線程的運行大概是下面的樣子。

上圖的綠色部分是程序的運行時間,紅色部分是等待時間。可以看到,由于I/O操作很慢,所以這個線程的大部分運行時間都在空等I/O操作的返回結果。這種運行方式稱為"同步模式"(synchronous I/O)。
如果采用多線程,同時運行多個任務,那很可能就是下面這樣。

上圖表明,多線程不僅占用多倍的系統資源,也閑置多倍的資源,這顯然不合理。

上圖主線程的綠色部分,還是表示運行時間,而橙色部分表示空閑時間。每當遇到I/O的時候,主線程就讓Event Loop線程去通知相應的I/O程序,然后接著往后運行,所以不存在紅色的等待時間。等到I/O程序完成操作,Event Loop線程再把結果返回主線程。主線程就調用事先設定的回調函數,完成整個任務。
可以看到,由于多出了橙色的空閑時間,所以主線程得以運行更多的任務,這就提高了效率。這種運行方式稱為"[異步模式](http://en.wikipedia.org/wiki/Asynchronous_I/O)"(asynchronous I/O)。
這正是JavaScript語言的運行方式。單線程模型雖然對JavaScript構成了很大的限制,但也因此使它具備了其他語言不具備的優勢。如果部署得好,JavaScript程序是不會出現堵塞的,這就是為什么node.js平臺可以用很少的資源,應付大流量訪問的原因。
## 任務隊列
如果有大量的異步任務(實際情況就是這樣),它們會在“任務隊列”中注冊大量的事件。這些事件排成隊列,等候進入主線程。本質上,“任務隊列”就是一個事件“先進先出”的數據結構。比如,點擊鼠標就產生一些列事件,mousedown事件排在mouseup事件前面,mouseup事件又排在click事件的前面。
## 參考鏈接
* John Dalziel,?[The race for speed part 2: How JavaScript compilers work](http://creativejs.com/2013/06/the-race-for-speed-part-2-how-javascript-compilers-work/)
* Jake Archibald,[Deep dive into the murky waters of script loading](http://www.html5rocks.com/en/tutorials/speed/script-loading/)
* Mozilla Developer Network,?[window.setTimeout](https://developer.mozilla.org/en-US/docs/Web/API/window.setTimeout)
* Remy Sharp,?[Throttling function calls](http://remysharp.com/2010/07/21/throttling-function-calls/)
* Ayman Farhat,?[An alternative to Javascript's evil setInterval](http://www.thecodeship.com/web-development/alternative-to-javascript-evil-setinterval/)
* Ilya Grigorik,?[Script-injected "async scripts" considered harmful](https://www.igvita.com/2014/05/20/script-injected-async-scripts-considered-harmful/)
* Axel Rauschmayer,?[ECMAScript 6 promises (1/2): foundations](http://www.2ality.com/2014/09/es6-promises-foundations.html)
* Daniel Imms,?[async vs defer attributes](http://www.growingwiththeweb.com/2014/02/async-vs-defer-attributes.html)
* Craig Buckler,?[Load Non-blocking JavaScript with HTML5 Async and Defer](http://www.sitepoint.com/non-blocking-async-defer/)
- 第一章 導論
- 1.1 前言
- 1.2 為什么學習JavaScript?
- 1.3 JavaScript的歷史
- 第二章 基本語法
- 2.1 語法概述
- 2.2 數值
- 2.3 字符串
- 2.4 對象
- 2.5 數組
- 2.6 函數
- 2.7 運算符
- 2.8 數據類型轉換
- 2.9 錯誤處理機制
- 2.10 JavaScript 編程風格
- 第三章 標準庫
- 3.1 Object對象
- 3.2 Array 對象
- 3.3 包裝對象和Boolean對象
- 3.4 Number對象
- 3.5 String對象
- 3.6 Math對象
- 3.7 Date對象
- 3.8 RegExp對象
- 3.9 JSON對象
- 3.10 ArrayBuffer:類型化數組
- 第四章 面向對象編程
- 4.1 概述
- 4.2 封裝
- 4.3 繼承
- 4.4 模塊化編程
- 第五章 DOM
- 5.1 Node節點
- 5.2 document節點
- 5.3 Element對象
- 5.4 Text節點和DocumentFragment節點
- 5.5 Event對象
- 5.6 CSS操作
- 5.7 Mutation Observer
- 第六章 瀏覽器對象
- 6.1 瀏覽器的JavaScript引擎
- 6.2 定時器
- 6.3 window對象
- 6.4 history對象
- 6.5 Ajax
- 6.6 同域限制和window.postMessage方法
- 6.7 Web Storage:瀏覽器端數據儲存機制
- 6.8 IndexedDB:瀏覽器端數據庫
- 6.9 Web Notifications API
- 6.10 Performance API
- 6.11 移動設備API
- 第七章 HTML網頁的API
- 7.1 HTML網頁元素
- 7.2 Canvas API
- 7.3 SVG 圖像
- 7.4 表單
- 7.5 文件和二進制數據的操作
- 7.6 Web Worker
- 7.7 SSE:服務器發送事件
- 7.8 Page Visibility API
- 7.9 Fullscreen API:全屏操作
- 7.10 Web Speech
- 7.11 requestAnimationFrame
- 7.12 WebSocket
- 7.13 WebRTC
- 7.14 Web Components
- 第八章 開發工具
- 8.1 console對象
- 8.2 PhantomJS
- 8.3 Bower:客戶端庫管理工具
- 8.4 Grunt:任務自動管理工具
- 8.5 Gulp:任務自動管理工具
- 8.6 Browserify:瀏覽器加載Node.js模塊
- 8.7 RequireJS和AMD規范
- 8.8 Source Map
- 8.9 JavaScript 程序測試
- 第九章 JavaScript高級語法
- 9.1 Promise對象
- 9.2 有限狀態機
- 9.3 MVC框架與Backbone.js
- 9.4 嚴格模式
- 9.5 ECMAScript 6 介紹
- 附錄
- 10.1 JavaScript API列表
- 草稿一:函數庫
- 11.1 Underscore.js
- 11.2 Modernizr
- 11.3 Datejs
- 11.4 D3.js
- 11.5 設計模式
- 11.6 排序算法
- 草稿二:jQuery
- 12.1 jQuery概述
- 12.2 jQuery工具方法
- 12.3 jQuery插件開發
- 12.4 jQuery.Deferred對象
- 12.5 如何做到 jQuery-free?
- 草稿三:Node.js
- 13.1 Node.js 概述
- 13.2 CommonJS規范
- 13.3 package.json文件
- 13.4 npm模塊管理器
- 13.5 fs 模塊
- 13.6 Path模塊
- 13.7 process對象
- 13.8 Buffer對象
- 13.9 Events模塊
- 13.10 stream接口
- 13.11 Child Process模塊
- 13.12 Http模塊
- 13.13 assert 模塊
- 13.14 Cluster模塊
- 13.15 os模塊
- 13.16 Net模塊和DNS模塊
- 13.17 Express框架
- 13.18 Koa 框架