?
本文檔使用 PHP中文網(wǎng)手冊(cè) 發(fā)布
JavaScript代碼嵌入網(wǎng)頁的方法
直接添加代碼塊
加載外部腳本
行內(nèi)代碼
外部腳本的加載
網(wǎng)頁底部加載
多個(gè)腳本的加載
defer屬性
async屬性
腳本的動(dòng)態(tài)嵌入
加載使用的協(xié)議
JavaScript虛擬機(jī)
單線程模型
Event Loop
任務(wù)隊(duì)列
參考鏈接
瀏覽器通過內(nèi)置的JavaScript引擎,讀取網(wǎng)頁中的代碼,對(duì)其處理后運(yùn)行。
在網(wǎng)頁中嵌入JavaScript代碼有多種方法。
通過script標(biāo)簽,可以直接將JavaScript代碼嵌入網(wǎng)頁。
<script> // some JavaScript code </script>
script標(biāo)簽也可以指定加載外部的腳本文件。
<script src="example.js"></script>
如果腳本文件使用了非英語字符,還應(yīng)該注明編碼。
<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代碼混寫在一起,非常不利于代碼管理,不建議使用。
正常的網(wǎng)頁加載流程是這樣的。
瀏覽器一邊下載HTML網(wǎng)頁,一邊開始解析
解析過程中,發(fā)現(xiàn)script標(biāo)簽
暫停解析,下載script標(biāo)簽中的外部腳本
下載完成,執(zhí)行腳本
恢復(fù)往下解析HTML網(wǎng)頁
也就是說,加載外部腳本時(shí),瀏覽器會(huì)暫停頁面渲染,等待腳本下載并執(zhí)行完成后,再繼續(xù)渲染。如果加載時(shí)間很長(zhǎng)(比如一直無法完成下載),就會(huì)造成網(wǎng)頁長(zhǎng)時(shí)間失去響應(yīng),瀏覽器就會(huì)呈現(xiàn)“假死”狀態(tài),失去響應(yīng),這被稱為“阻塞效應(yīng)”。這樣設(shè)計(jì)是因?yàn)镴avaScript代碼可能會(huì)修改頁面,所以必須等它執(zhí)行完才能繼續(xù)渲染。為了避免這種情況,較好的做法是將script標(biāo)簽都放在頁面底部,而不是頭部。當(dāng)然,如果某些腳本代碼非常重要,一定要放在頁面頭部的話,最好直接將代碼嵌入頁面,而不是連接外部腳本文件,這樣能縮短加載時(shí)間。
將腳本文件都放在網(wǎng)頁尾部加載,還有一個(gè)好處。在DOM結(jié)構(gòu)生成之前就調(diào)用DOM,JavaScript會(huì)報(bào)錯(cuò),如果腳本都在網(wǎng)頁尾部加載,就不存在這個(gè)問題,因?yàn)檫@時(shí)DOM肯定已經(jīng)生成了。
<head> <script> console.log(document.body.innerHTML); </script> </head>
上面代碼執(zhí)行時(shí)會(huì)報(bào)錯(cuò),因?yàn)榇藭r(shí)body元素還未生成。
一種解決方法是設(shè)定DOMContentLoaded事件的回調(diào)函數(shù)。
<head> <script> document.addEventListener("DOMContentLoaded", function(event) { console.log(document.body.innerHTML); }); </script> </head>
另一種解決方法是,使用script標(biāo)簽的onload屬性。當(dāng)script標(biāo)簽指定的外部腳本文件下載和解析完成,會(huì)觸發(fā)一個(gè)load事件,可以為這個(gè)事件指定回調(diào)函數(shù)。
<script src="jquery.min.js" onload="console.log(document.body.innerHTML)"> </script>
但是,如果將腳本放在頁面底部,就可以完全按照正常的方式寫,上面兩種方式都不需要。
<body> <!-- 其他代碼 --> <script> console.log(document.body.innerHTML); </script> </body>
如果有多個(gè)script標(biāo)簽,比如下面這樣。
<script src="1.js"></script> <script src="2.js"></script>
瀏覽器會(huì)同時(shí)平行下載1.js和2.js,但是執(zhí)行時(shí)會(huì)保證先執(zhí)行1.js,然后再執(zhí)行2.js,即使后者先下載完成,也是如此。也就是說,腳本的執(zhí)行順序由它們?cè)陧撁嬷械某霈F(xiàn)順序決定,這是為了保證腳本之間的依賴關(guān)系不受到破壞。
當(dāng)然,加載這兩個(gè)腳本都會(huì)產(chǎn)生“阻塞效應(yīng)”,必須等到它們都加載完成,瀏覽器才會(huì)繼續(xù)頁面渲染。
此外,對(duì)于來自同一個(gè)域名的資源,比如腳本文件、樣式表文件、圖片文件等,瀏覽器一般最多同時(shí)下載六個(gè)。如果是來自不同域名的資源,就沒有這個(gè)限制。所以,通常把靜態(tài)文件放在不同的域名之下,以加快下載速度。
為了解決腳本文件下載阻塞網(wǎng)頁渲染的問題,一個(gè)方法是加入defer屬性。
<script src="1.js" defer></script> <script src="2.js" defer></script>
defer屬性的運(yùn)行過程是這樣的。
瀏覽器開始解析HTML網(wǎng)頁
解析過程中,發(fā)現(xiàn)帶有defer屬性的script標(biāo)簽
瀏覽器繼續(xù)往下解析HTML網(wǎng)頁,同時(shí)并行下載script標(biāo)簽中的外部腳本
瀏覽器完成解析HTML網(wǎng)頁,此時(shí)再執(zhí)行下載的腳本
有了defer屬性,瀏覽器下載腳本文件的時(shí)候,不會(huì)阻塞頁面渲染。下載的腳本文件在DOMContentLoaded事件觸發(fā)前執(zhí)行(即剛剛讀取完標(biāo)簽),而且可以保證執(zhí)行順序就是它們?cè)陧撁嫔铣霈F(xiàn)的順序。但是,瀏覽器對(duì)這個(gè)屬性的支持不夠理想,IE(<=9)還有一個(gè)bug,無法保證2.js一定在1.js之后執(zhí)行。如果需要支持老版本的IE,且腳本之間有依賴關(guān)系,建議不要使用defer屬性。
對(duì)于內(nèi)置而不是連接外部腳本的script標(biāo)簽,以及動(dòng)態(tài)生成的script標(biāo)簽,defer屬性不起作用。
解決“阻塞效應(yīng)”的另一個(gè)方法是加入async屬性。
<script src="1.js" async></script> <script src="2.js" async></script>
async屬性的運(yùn)行過程是這樣的。
瀏覽器開始解析HTML網(wǎng)頁
解析過程中,發(fā)現(xiàn)帶有async屬性的script標(biāo)簽
瀏覽器繼續(xù)往下解析HTML網(wǎng)頁,同時(shí)并行下載script標(biāo)簽中的外部腳本
腳本下載完成,瀏覽器暫停解析HTML網(wǎng)頁,開始執(zhí)行下載的腳本
腳本執(zhí)行完畢,瀏覽器恢復(fù)解析HTML網(wǎng)頁
async屬性可以保證腳本下載的同時(shí),瀏覽器繼續(xù)渲染。需要注意的是,一旦采用這個(gè)屬性,就無法保證腳本的執(zhí)行順序。哪個(gè)腳本先下載結(jié)束,就先執(zhí)行那個(gè)腳本。使用async屬性的腳本文件中,不應(yīng)該使用document.write方法。IE 10支持async屬性,低于這個(gè)版本的IE都不支持。
defer屬性和async屬性到底應(yīng)該使用哪一個(gè)?一般來說,如果腳本之間沒有依賴關(guān)系,就使用async屬性,如果腳本之間有依賴關(guān)系,就使用defer屬性。如果同時(shí)使用async和defer屬性,后者不起作用,瀏覽器行為由async屬性決定。
除了用靜態(tài)的script標(biāo)簽,還可以動(dòng)態(tài)嵌入script標(biāo)簽。
['1.js', '2.js'].forEach(function(src) { var script = document.createElement('script'); script.src = src; document.head.appendChild(script); });
這種方法的好處是,動(dòng)態(tài)生成的script標(biāo)簽不會(huì)阻塞頁面渲染,也就不會(huì)造成瀏覽器假死。但是問題在于,這種方法無法保證腳本的執(zhí)行順序,哪個(gè)腳本文件先下載完成,就先執(zhí)行哪個(gè)。
如果想避免這個(gè)問題,可以設(shè)置async屬性為false。
['1.js', '2.js'].forEach(function(src) { var script = document.createElement('script'); script.src = src; script.async = false; document.head.appendChild(script); });
上面的代碼依然不會(huì)阻塞頁面渲染,而且可以保證2.js在1.js后面執(zhí)行。不過需要注意的是,在這段代碼后面加載的腳本文件,會(huì)因此都等待2.js執(zhí)行完成后再執(zhí)行。
我們可以把上面的寫法,封裝成一個(gè)函數(shù)。
(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'); }());
此外,動(dòng)態(tài)嵌入還有一個(gè)地方需要注意。動(dòng)態(tài)嵌入必須等待CSS文件加載完成后,才會(huì)去下載外部腳本文件。靜態(tài)加載就不存在這個(gè)問題,script標(biāo)簽指定的外部腳本文件,都是與CSS文件同時(shí)并發(fā)下載的。
如果不指定協(xié)議,瀏覽器默認(rèn)采用HTTP協(xié)議下載。
<script src="example.js"></script>
上面的example.js默認(rèn)就是采用http協(xié)議下載,如果要采用HTTPs協(xié)議下載,必需寫明(假定服務(wù)器支持)。
<script src="https://example.js"></script>
但是有時(shí)我們會(huì)希望,根據(jù)頁面本身的協(xié)議來決定加載協(xié)議,這時(shí)可以采用下面的寫法。
<script src="//example.js"></script>
JavaScript是一種解釋型語言,也就是說,它不需要編譯,可以由解釋器實(shí)時(shí)運(yùn)行。這樣的好處是運(yùn)行和修改都比較方便,刷新頁面就可以重新解釋;缺點(diǎn)是每次運(yùn)行都要調(diào)用解釋器,系統(tǒng)開銷較大,運(yùn)行速度慢于編譯型語言。為了提高運(yùn)行速度,目前的瀏覽器都將JavaScript進(jìn)行一定程度的編譯,生成類似字節(jié)碼(bytecode)的中間代碼,以提高運(yùn)行速度。
早期,瀏覽器內(nèi)部對(duì)JavaScript的處理過程如下:
讀取代碼,進(jìn)行詞法分析(Lexical analysis),將代碼分解成詞元(token)。
對(duì)詞元進(jìn)行語法分析(parsing),將代碼整理成“語法樹”(syntax tree)。
使用“翻譯器”(translator),將代碼轉(zhuǎn)為字節(jié)碼(bytecode)。
使用“字節(jié)碼解釋器”(bytecode interpreter),將字節(jié)碼轉(zhuǎn)為機(jī)器碼。
逐行解釋將字節(jié)碼轉(zhuǎn)為機(jī)器碼,是很低效的。為了提高運(yùn)行速度,現(xiàn)代瀏覽器改為采用“即時(shí)編譯”(Just In Time compiler,縮寫JIT),即字節(jié)碼只在運(yùn)行時(shí)編譯,用到哪一行就編譯哪一行,并且把編譯結(jié)果緩存(inline cache)。通常,一個(gè)程序被經(jīng)常用到的,只是其中一小部分代碼,有了緩存的編譯結(jié)果,整個(gè)程序的運(yùn)行速度就會(huì)顯著提升。
不同的瀏覽器有不同的編譯策略。有的瀏覽器只編譯最經(jīng)常用到的部分,比如循環(huán)的部分;有的瀏覽器索性省略了字節(jié)碼的翻譯步驟,直接編譯成機(jī)器碼,比如chrome瀏覽器的V8引擎。
字節(jié)碼不能直接運(yùn)行,而是運(yùn)行在一個(gè)虛擬機(jī)(Virtual Machine)之上,一般也把虛擬機(jī)稱為JavaScript引擎。因?yàn)镴avaScript運(yùn)行時(shí)未必有字節(jié)碼,所以JavaScript虛擬機(jī)并不完全基于字節(jié)碼,而是部分基于源碼,即只要有可能,就通過JIT(just in time)編譯器直接把源碼編譯成機(jī)器碼運(yùn)行,省略字節(jié)碼步驟。這一點(diǎn)與其他采用虛擬機(jī)(比如Java)的語言不盡相同。這樣做的目的,是為了盡可能地優(yōu)化代碼、提高性能。下面是目前最常見的一些JavaScript虛擬機(jī):
[Chakra](http://en.wikipedia.org/wiki/Chakra_(JScript_engine))(Microsoft(Microsoft) Internet Explorer)
Nitro/JavaScript Core (Safari)
Carakan (Opera)
SpiderMonkey(Firefox)
[V8](http://en.wikipedia.org/wiki/V8_(JavaScript_engine)) (Chrome, Chromium)
JavaScript采用單線程模型,也就是說,所有的任務(wù)都在一個(gè)線程里運(yùn)行。這意味著,一次只能運(yùn)行一個(gè)任務(wù),其他任務(wù)都必須在后面排隊(duì)等待。
JavaScript之所以采用單線程,而不是多線程,跟歷史有關(guān)系。JavaScript從誕生起就是單線程,原因是不想讓瀏覽器變得太復(fù)雜,因?yàn)槎嗑€程需要共享資源、且有可能修改彼此的運(yùn)行結(jié)果,對(duì)于一種網(wǎng)頁腳本語言來說,這就太復(fù)雜了。比如,假定JavaScript同時(shí)有兩個(gè)線程,一個(gè)線程在某個(gè)DOM節(jié)點(diǎn)上添加內(nèi)容,另一個(gè)線程刪除了這個(gè)節(jié)點(diǎn),這時(shí)瀏覽器應(yīng)該以哪個(gè)線程為準(zhǔn)?所以,為了避免復(fù)雜性,從一誕生,JavaScript就是單線程,這已經(jīng)成了這門語言的核心特征,將來也不會(huì)改變。
為了利用多核CPU的計(jì)算能力,HTML5提出Web Worker標(biāo)準(zhǔn),允許JavaScript腳本創(chuàng)建多個(gè)線程,但是子線程完全受主線程控制,且不得操作DOM。所以,這個(gè)新標(biāo)準(zhǔn)并沒有改變JavaScript單線程的本質(zhì)。
單線程模型帶來了一些問題,主要是新的任務(wù)被加在隊(duì)列的尾部,只有前面的所有任務(wù)運(yùn)行結(jié)束,才會(huì)輪到它執(zhí)行。如果有一個(gè)任務(wù)特別耗時(shí),后面的任務(wù)都會(huì)停在那里等待,造成瀏覽器失去響應(yīng),又稱“假死”。為了避免“假死”,當(dāng)某個(gè)操作在一定時(shí)間后仍無法結(jié)束,瀏覽器就會(huì)跳出提示框,詢問用戶是否要強(qiáng)行停止腳本運(yùn)行。
如果排隊(duì)是因?yàn)橛?jì)算量大,CPU忙不過來,倒也算了,但是很多時(shí)候CPU是閑著的,因?yàn)镮O設(shè)備(輸入輸出設(shè)備)很慢(比如Ajax操作從網(wǎng)絡(luò)讀取數(shù)據(jù)),不得不等著結(jié)果出來,再往下執(zhí)行。JavaScript語言的設(shè)計(jì)者意識(shí)到,這時(shí)CPU完全可以不管IO設(shè)備,掛起處于等待中的任務(wù),先運(yùn)行排在后面的任務(wù)。等到IO設(shè)備返回了結(jié)果,再回過頭,把掛起的任務(wù)繼續(xù)執(zhí)行下去。這種機(jī)制就是JavaScript內(nèi)部采用的Event Loop。
所謂Event Loop,指的是一種內(nèi)部循環(huán),用來排列和處理事件,以及執(zhí)行函數(shù)。Wikipedia的定義是:“Event Loop是一個(gè)程序結(jié)構(gòu),用于等待和發(fā)送消息和事件。(a programming construct that waits for and dispatches events or messages in a program.)”
所有任務(wù)可以分成兩種,一種是同步任務(wù)(synchronous),另一種是異步任務(wù)(asynchronous)。同步任務(wù)指的是,在主線程上排隊(duì)執(zhí)行的任務(wù),只有前一個(gè)任務(wù)執(zhí)行完畢,才能執(zhí)行后一個(gè)任務(wù);異步任務(wù)指的是,不進(jìn)入主線程、而進(jìn)入“任務(wù)隊(duì)列”(task queue)的任務(wù),只有“任務(wù)隊(duì)列”通知主線程,某個(gè)異步任務(wù)可以執(zhí)行了,該任務(wù)才會(huì)進(jìn)入主線程執(zhí)行。
以Ajax操作為例,它可以當(dāng)作同步任務(wù)處理,也可以當(dāng)作異步任務(wù)處理,由開發(fā)者決定。如果是同步任務(wù),主線程就等著Ajax操作返回結(jié)果,再往下執(zhí)行;如果是異步任務(wù),該任務(wù)直接進(jìn)入“任務(wù)隊(duì)列”,主線程跳過Ajax操作,直接往下執(zhí)行,等到Ajax操作有了結(jié)果,主線程再執(zhí)行對(duì)應(yīng)的回調(diào)函數(shù)。
想要理解Event Loop,就要從程序的運(yùn)行模式講起。運(yùn)行以后的程序叫做"進(jìn)程"(process),一般情況下,一個(gè)進(jìn)程一次只能執(zhí)行一個(gè)任務(wù)。如果有很多任務(wù)需要執(zhí)行,不外乎三種解決方法。
排隊(duì)。因?yàn)橐粋€(gè)進(jìn)程一次只能執(zhí)行一個(gè)任務(wù),只好等前面的任務(wù)執(zhí)行完了,再執(zhí)行后面的任務(wù)。
新建進(jìn)程。使用fork命令,為每個(gè)任務(wù)新建一個(gè)進(jìn)程。
新建線程。因?yàn)檫M(jìn)程太耗費(fèi)資源,所以如今的程序往往允許一個(gè)進(jìn)程包含多個(gè)線程,由線程去完成任務(wù)。
如果某個(gè)任務(wù)很耗時(shí),比如涉及很多I/O(輸入/輸出)操作,那么線程的運(yùn)行大概是下面的樣子。
上圖的綠色部分是程序的運(yùn)行時(shí)間,紅色部分是等待時(shí)間??梢钥吹剑捎贗/O操作很慢,所以這個(gè)線程的大部分運(yùn)行時(shí)間都在空等I/O操作的返回結(jié)果。這種運(yùn)行方式稱為"同步模式"(synchronous I/O)。
如果采用多線程,同時(shí)運(yùn)行多個(gè)任務(wù),那很可能就是下面這樣。
上圖表明,多線程不僅占用多倍的系統(tǒng)資源,也閑置多倍的資源,這顯然不合理。
上圖主線程的綠色部分,還是表示運(yùn)行時(shí)間,而橙色部分表示空閑時(shí)間。每當(dāng)遇到I/O的時(shí)候,主線程就讓Event Loop線程去通知相應(yīng)的I/O程序,然后接著往后運(yùn)行,所以不存在紅色的等待時(shí)間。等到I/O程序完成操作,Event Loop線程再把結(jié)果返回主線程。主線程就調(diào)用事先設(shè)定的回調(diào)函數(shù),完成整個(gè)任務(wù)。
可以看到,由于多出了橙色的空閑時(shí)間,所以主線程得以運(yùn)行更多的任務(wù),這就提高了效率。這種運(yùn)行方式稱為"異步模式"(asynchronous I/O)。
這正是JavaScript語言的運(yùn)行方式。單線程模型雖然對(duì)JavaScript構(gòu)成了很大的限制,但也因此使它具備了其他語言不具備的優(yōu)勢(shì)。如果部署得好,JavaScript程序是不會(huì)出現(xiàn)堵塞的,這就是為什么node.js平臺(tái)可以用很少的資源,應(yīng)付大流量訪問的原因。
如果有大量的異步任務(wù)(實(shí)際情況就是這樣),它們會(huì)在“任務(wù)隊(duì)列”中注冊(cè)大量的事件。這些事件排成隊(duì)列,等候進(jìn)入主線程。本質(zhì)上,“任務(wù)隊(duì)列”就是一個(gè)事件“先進(jìn)先出”的數(shù)據(jù)結(jié)構(gòu)。比如,點(diǎn)擊鼠標(biāo)就產(chǎn)生一些列事件,mousedown事件排在mouseup事件前面,mouseup事件又排在click事件的前面。
John Dalziel, The race for speed part 2: How JavaScript compilers work
Jake Archibald,Deep dive into the murky waters of script loading
Mozilla Developer Network, window.setTimeout
Remy Sharp, Throttling function calls
Ayman Farhat, An alternative to Javascript's evil setInterval
Ilya Grigorik, Script-injected "async scripts" considered harmful
Axel Rauschmayer, ECMAScript 6 promises (1/2): foundations
Daniel Imms, async vs defer attributes
Craig Buckler, Load Non-blocking JavaScript with HTML5 Async and Defer