W3Cschool
恭喜您成為首批注冊(cè)用戶(hù)
獲得88經(jīng)驗(yàn)值獎(jiǎng)勵(lì)
瀏覽器通過(guò)內(nèi)置的JavaScript引擎,讀取網(wǎng)頁(yè)中的代碼,對(duì)其處理后運(yùn)行。
在網(wǎng)頁(yè)中嵌入JavaScript代碼有多種方法。
通過(guò)script標(biāo)簽,可以直接將JavaScript代碼嵌入網(wǎng)頁(yè)。
<script>
// some JavaScript code
</script>
script標(biāo)簽也可以指定加載外部的腳本文件。
<script src="example.js"></script>
如果腳本文件使用了非英語(yǔ)字符,還應(yīng)該注明編碼。
<script charset="utf-8" src="https://atts.w3cschool.cn/attachments/image/cimg/pre>
加載外部腳本和直接添加代碼塊,這兩種方法不能混用。下面代碼的console.log語(yǔ)句直接被忽略。
<script charset="utf-8" src="example.js">
console.log('Hello World!');
</script>
行內(nèi)代碼
除了上面兩種方法,HTML語(yǔ)言允許在某些元素的事件屬性和a元素的href屬性中,直接寫(xiě)入JavaScript。
<div onclick="alert('Hello')"></div>
<a href="javascript:alert('Hello')"></a>
這種寫(xiě)法將HTML代碼與JavaScript代碼混寫(xiě)在一起,非常不利于代碼管理,不建議使用。
外部腳本的加載
網(wǎng)頁(yè)底部加載
正常的網(wǎng)頁(yè)加載流程是這樣的。
- 瀏覽器一邊下載HTML網(wǎng)頁(yè),一邊開(kāi)始解析
- 解析過(guò)程中,發(fā)現(xiàn)script標(biāo)簽
- 暫停解析,下載script標(biāo)簽中的外部腳本
- 下載完成,執(zhí)行腳本
- 恢復(fù)往下解析HTML網(wǎng)頁(yè)
也就是說(shuō),加載外部腳本時(shí),瀏覽器會(huì)暫停頁(yè)面渲染,等待腳本下載并執(zhí)行完成后,再繼續(xù)渲染。如果加載時(shí)間很長(zhǎng)(比如一直無(wú)法完成下載),就會(huì)造成網(wǎng)頁(yè)長(zhǎng)時(shí)間失去響應(yīng),瀏覽器就會(huì)呈現(xiàn)“假死”狀態(tài),失去響應(yīng),這被稱(chēng)為“阻塞效應(yīng)”。這樣設(shè)計(jì)是因?yàn)镴avaScript代碼可能會(huì)修改頁(yè)面,所以必須等它執(zhí)行完才能繼續(xù)渲染。為了避免這種情況,較好的做法是將script標(biāo)簽都放在頁(yè)面底部,而不是頭部。當(dāng)然,如果某些腳本代碼非常重要,一定要放在頁(yè)面頭部的話(huà),最好直接將代碼嵌入頁(yè)面,而不是連接外部腳本文件,這樣能縮短加載時(shí)間。
將腳本文件都放在網(wǎng)頁(yè)尾部加載,還有一個(gè)好處。在DOM結(jié)構(gòu)生成之前就調(diào)用DOM,JavaScript會(huì)報(bào)錯(cuò),如果腳本都在網(wǎng)頁(yè)尾部加載,就不存在這個(gè)問(wèn)題,因?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>
但是,如果將腳本放在頁(yè)面底部,就可以完全按照正常的方式寫(xiě),上面兩種方式都不需要。
<body>
<!-- 其他代碼 -->
<script>
console.log(document.body.innerHTML);
</script>
</body>
多個(gè)腳本的加載
如果有多個(gè)script標(biāo)簽,比如下面這樣。
<script src="https://atts.w3cschool.cn/attachments/image/cimg/script>
<script src="2.js"></script>
瀏覽器會(huì)同時(shí)平行下載1.js和2.js,但是執(zhí)行時(shí)會(huì)保證先執(zhí)行1.js,然后再執(zhí)行2.js,即使后者先下載完成,也是如此。也就是說(shuō),腳本的執(zhí)行順序由它們?cè)陧?yè)面中的出現(xiàn)順序決定,這是為了保證腳本之間的依賴(lài)關(guān)系不受到破壞。
當(dāng)然,加載這兩個(gè)腳本都會(huì)產(chǎn)生“阻塞效應(yīng)”,必須等到它們都加載完成,瀏覽器才會(huì)繼續(xù)頁(yè)面渲染。
此外,對(duì)于來(lái)自同一個(gè)域名的資源,比如腳本文件、樣式表文件、圖片文件等,瀏覽器一般最多同時(shí)下載六個(gè)。如果是來(lái)自不同域名的資源,就沒(méi)有這個(gè)限制。所以,通常把靜態(tài)文件放在不同的域名之下,以加快下載速度。
defer屬性
為了解決腳本文件下載阻塞網(wǎng)頁(yè)渲染的問(wèn)題,一個(gè)方法是加入defer屬性。
<script src="https://atts.w3cschool.cn/attachments/image/cimg/script>
<script src="2.js" defer></script>
defer屬性的運(yùn)行過(guò)程是這樣的。
- 瀏覽器開(kāi)始解析HTML網(wǎng)頁(yè)
- 解析過(guò)程中,發(fā)現(xiàn)帶有defer屬性的script標(biāo)簽
- 瀏覽器繼續(xù)往下解析HTML網(wǎng)頁(yè),同時(shí)并行下載script標(biāo)簽中的外部腳本
- 瀏覽器完成解析HTML網(wǎng)頁(yè),此時(shí)再執(zhí)行下載的腳本
有了defer屬性,瀏覽器下載腳本文件的時(shí)候,不會(huì)阻塞頁(yè)面渲染。下載的腳本文件在DOMContentLoaded事件觸發(fā)前執(zhí)行(即剛剛讀取完標(biāo)簽),而且可以保證執(zhí)行順序就是它們?cè)陧?yè)面上出現(xiàn)的順序。但是,瀏覽器對(duì)這個(gè)屬性的支持不夠理想,IE(<=9)還有一個(gè)bug,無(wú)法保證2.js一定在1.js之后執(zhí)行。如果需要支持老版本的IE,且腳本之間有依賴(lài)關(guān)系,建議不要使用defer屬性。
對(duì)于內(nèi)置而不是連接外部腳本的script標(biāo)簽,以及動(dòng)態(tài)生成的script標(biāo)簽,defer屬性不起作用。
async屬性
解決“阻塞效應(yīng)”的另一個(gè)方法是加入async屬性。
<script src="https://atts.w3cschool.cn/attachments/image/cimg/script>
<script src="2.js" async></script>
async屬性的運(yùn)行過(guò)程是這樣的。
- 瀏覽器開(kāi)始解析HTML網(wǎng)頁(yè)
- 解析過(guò)程中,發(fā)現(xiàn)帶有async屬性的script標(biāo)簽
- 瀏覽器繼續(xù)往下解析HTML網(wǎng)頁(yè),同時(shí)并行下載script標(biāo)簽中的外部腳本
- 腳本下載完成,瀏覽器暫停解析HTML網(wǎng)頁(yè),開(kāi)始執(zhí)行下載的腳本
- 腳本執(zhí)行完畢,瀏覽器恢復(fù)解析HTML網(wǎng)頁(yè)
async屬性可以保證腳本下載的同時(shí),瀏覽器繼續(xù)渲染。需要注意的是,一旦采用這個(gè)屬性,就無(wú)法保證腳本的執(zhí)行順序。哪個(gè)腳本先下載結(jié)束,就先執(zhí)行那個(gè)腳本。使用async屬性的腳本文件中,不應(yīng)該使用document.write方法。IE 10支持async屬性,低于這個(gè)版本的IE都不支持。
defer屬性和async屬性到底應(yīng)該使用哪一個(gè)?一般來(lái)說(shuō),如果腳本之間沒(méi)有依賴(lài)關(guān)系,就使用async屬性,如果腳本之間有依賴(lài)關(guān)系,就使用defer屬性。如果同時(shí)使用async和defer屬性,后者不起作用,瀏覽器行為由async屬性決定。
腳本的動(dòng)態(tài)嵌入
除了用靜態(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ì)阻塞頁(yè)面渲染,也就不會(huì)造成瀏覽器假死。但是問(wèn)題在于,這種方法無(wú)法保證腳本的執(zhí)行順序,哪個(gè)腳本文件先下載完成,就先執(zhí)行哪個(gè)。
如果想避免這個(gè)問(wèn)題,可以設(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ì)阻塞頁(yè)面渲染,而且可以保證2.js在1.js后面執(zhí)行。不過(guò)需要注意的是,在這段代碼后面加載的腳本文件,會(huì)因此都等待2.js執(zhí)行完成后再執(zhí)行。
我們可以把上面的寫(xiě)法,封裝成一個(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è)問(wèn)題,script標(biāo)簽指定的外部腳本文件,都是與CSS文件同時(shí)并發(fā)下載的。
加載使用的協(xié)議
如果不指定協(xié)議,瀏覽器默認(rèn)采用HTTP協(xié)議下載。
<script src="https://atts.w3cschool.cn/attachments/image/cimg/pre>
上面的example.js默認(rèn)就是采用http協(xié)議下載,如果要采用HTTPs協(xié)議下載,必需寫(xiě)明(假定服務(wù)器支持)。
<script src="https://atts.w3cschool.cn/attachments/image/cimg/example.js"></script>
但是有時(shí)我們會(huì)希望,根據(jù)頁(yè)面本身的協(xié)議來(lái)決定加載協(xié)議,這時(shí)可以采用下面的寫(xiě)法。
<script src="https://atts.w3cschool.cn/attachments/image/cimg/example.js"></script>
JavaScript虛擬機(jī)
JavaScript是一種解釋型語(yǔ)言,也就是說(shuō),它不需要編譯,可以由解釋器實(shí)時(shí)運(yùn)行。這樣的好處是運(yùn)行和修改都比較方便,刷新頁(yè)面就可以重新解釋?zhuān)蝗秉c(diǎn)是每次運(yùn)行都要調(diào)用解釋器,系統(tǒng)開(kāi)銷(xiāo)較大,運(yùn)行速度慢于編譯型語(yǔ)言。為了提高運(yùn)行速度,目前的瀏覽器都將JavaScript進(jìn)行一定程度的編譯,生成類(lèi)似字節(jié)碼(bytecode)的中間代碼,以提高運(yùn)行速度。
早期,瀏覽器內(nèi)部對(duì)JavaScript的處理過(guò)程如下:
- 讀取代碼,進(jìn)行詞法分析(Lexical analysis),將代碼分解成詞元(token)。
- 對(duì)詞元進(jìn)行語(yǔ)法分析(parsing),將代碼整理成“語(yǔ)法樹(shù)”(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,縮寫(xiě)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ī)稱(chēng)為JavaScript引擎。因?yàn)镴avaScript運(yùn)行時(shí)未必有字節(jié)碼,所以JavaScript虛擬機(jī)并不完全基于字節(jié)碼,而是部分基于源碼,即只要有可能,就通過(guò)JIT(just in time)編譯器直接把源碼編譯成機(jī)器碼運(yùn)行,省略字節(jié)碼步驟。這一點(diǎn)與其他采用虛擬機(jī)(比如Java)的語(yǔ)言不盡相同。這樣做的目的,是為了盡可能地優(yōu)化代碼、提高性能。下面是目前最常見(jiàn)的一些JavaScript虛擬機(jī):
- Chakra(Microsoft](http://en.wikipedia.org/wiki/Chakra_(JScript_engine%5C))(Microsoft) Internet Explorer)
- Nitro/JavaScript Core (Safari)
- Carakan (Opera)
- SpiderMonkey (Firefox)
- V8) (Chrome, Chromium)
單線(xiàn)程模型
JavaScript采用單線(xiàn)程模型,也就是說(shuō),所有的任務(wù)都在一個(gè)線(xiàn)程里運(yùn)行。這意味著,一次只能運(yùn)行一個(gè)任務(wù),其他任務(wù)都必須在后面排隊(duì)等待。
JavaScript之所以采用單線(xiàn)程,而不是多線(xiàn)程,跟歷史有關(guān)系。JavaScript從誕生起就是單線(xiàn)程,原因是不想讓瀏覽器變得太復(fù)雜,因?yàn)槎嗑€(xiàn)程需要共享資源、且有可能修改彼此的運(yùn)行結(jié)果,對(duì)于一種網(wǎng)頁(yè)腳本語(yǔ)言來(lái)說(shuō),這就太復(fù)雜了。比如,假定JavaScript同時(shí)有兩個(gè)線(xiàn)程,一個(gè)線(xiàn)程在某個(gè)DOM節(jié)點(diǎn)上添加內(nèi)容,另一個(gè)線(xiàn)程刪除了這個(gè)節(jié)點(diǎn),這時(shí)瀏覽器應(yīng)該以哪個(gè)線(xiàn)程為準(zhǔn)?所以,為了避免復(fù)雜性,從一誕生,JavaScript就是單線(xiàn)程,這已經(jīng)成了這門(mén)語(yǔ)言的核心特征,將來(lái)也不會(huì)改變。
為了利用多核CPU的計(jì)算能力,HTML5提出Web Worker標(biāo)準(zhǔn),允許JavaScript腳本創(chuàng)建多個(gè)線(xiàn)程,但是子線(xiàn)程完全受主線(xiàn)程控制,且不得操作DOM。所以,這個(gè)新標(biāo)準(zhǔn)并沒(méi)有改變JavaScript單線(xiàn)程的本質(zhì)。
單線(xiàn)程模型帶來(lái)了一些問(wèn)題,主要是新的任務(wù)被加在隊(duì)列的尾部,只有前面的所有任務(wù)運(yùn)行結(jié)束,才會(huì)輪到它執(zhí)行。如果有一個(gè)任務(wù)特別耗時(shí),后面的任務(wù)都會(huì)停在那里等待,造成瀏覽器失去響應(yīng),又稱(chēng)“假死”。為了避免“假死”,當(dāng)某個(gè)操作在一定時(shí)間后仍無(wú)法結(jié)束,瀏覽器就會(huì)跳出提示框,詢(xún)問(wèn)用戶(hù)是否要強(qiáng)行停止腳本運(yùn)行。
如果排隊(duì)是因?yàn)橛?jì)算量大,CPU忙不過(guò)來(lái),倒也算了,但是很多時(shí)候CPU是閑著的,因?yàn)镮O設(shè)備(輸入輸出設(shè)備)很慢(比如Ajax操作從網(wǎng)絡(luò)讀取數(shù)據(jù)),不得不等著結(jié)果出來(lái),再往下執(zhí)行。JavaScript語(yǔ)言的設(shè)計(jì)者意識(shí)到,這時(shí)CPU完全可以不管IO設(shè)備,掛起處于等待中的任務(wù),先運(yùn)行排在后面的任務(wù)。等到IO設(shè)備返回了結(jié)果,再回過(guò)頭,把掛起的任務(wù)繼續(xù)執(zhí)行下去。這種機(jī)制就是JavaScript內(nèi)部采用的Event Loop。
Event Loop
所謂Event Loop,指的是一種內(nèi)部循環(huán),用來(lái)排列和處理事件,以及執(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ù)指的是,在主線(xiàn)程上排隊(duì)執(zhí)行的任務(wù),只有前一個(gè)任務(wù)執(zhí)行完畢,才能執(zhí)行后一個(gè)任務(wù);異步任務(wù)指的是,不進(jìn)入主線(xiàn)程、而進(jìn)入“任務(wù)隊(duì)列”(task queue)的任務(wù),只有“任務(wù)隊(duì)列”通知主線(xiàn)程,某個(gè)異步任務(wù)可以執(zhí)行了,該任務(wù)才會(huì)進(jìn)入主線(xiàn)程執(zhí)行。
以Ajax操作為例,它可以當(dāng)作同步任務(wù)處理,也可以當(dāng)作異步任務(wù)處理,由開(kāi)發(fā)者決定。如果是同步任務(wù),主線(xiàn)程就等著Ajax操作返回結(jié)果,再往下執(zhí)行;如果是異步任務(wù),該任務(wù)直接進(jìn)入“任務(wù)隊(duì)列”,主線(xiàn)程跳過(guò)Ajax操作,直接往下執(zhí)行,等到Ajax操作有了結(jié)果,主線(xiàn)程再執(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)程。
- 新建線(xiàn)程。因?yàn)檫M(jìn)程太耗費(fèi)資源,所以如今的程序往往允許一個(gè)進(jìn)程包含多個(gè)線(xiàn)程,由線(xiàn)程去完成任務(wù)。
如果某個(gè)任務(wù)很耗時(shí),比如涉及很多I/O(輸入/輸出)操作,那么線(xiàn)程的運(yùn)行大概是下面的樣子。
上圖的綠色部分是程序的運(yùn)行時(shí)間,紅色部分是等待時(shí)間??梢钥吹?,由于I/O操作很慢,所以這個(gè)線(xiàn)程的大部分運(yùn)行時(shí)間都在空等I/O操作的返回結(jié)果。這種運(yùn)行方式稱(chēng)為"同步模式"(synchronous I/O)。
如果采用多線(xiàn)程,同時(shí)運(yùn)行多個(gè)任務(wù),那很可能就是下面這樣。
上圖表明,多線(xiàn)程不僅占用多倍的系統(tǒng)資源,也閑置多倍的資源,這顯然不合理。
上圖主線(xiàn)程的綠色部分,還是表示運(yùn)行時(shí)間,而橙色部分表示空閑時(shí)間。每當(dāng)遇到I/O的時(shí)候,主線(xiàn)程就讓Event Loop線(xiàn)程去通知相應(yīng)的I/O程序,然后接著往后運(yùn)行,所以不存在紅色的等待時(shí)間。等到I/O程序完成操作,Event Loop線(xiàn)程再把結(jié)果返回主線(xiàn)程。主線(xiàn)程就調(diào)用事先設(shè)定的回調(diào)函數(shù),完成整個(gè)任務(wù)。
可以看到,由于多出了橙色的空閑時(shí)間,所以主線(xiàn)程得以運(yùn)行更多的任務(wù),這就提高了效率。這種運(yùn)行方式稱(chēng)為"異步模式"(asynchronous I/O)。
這正是JavaScript語(yǔ)言的運(yùn)行方式。單線(xiàn)程模型雖然對(duì)JavaScript構(gòu)成了很大的限制,但也因此使它具備了其他語(yǔ)言不具備的優(yōu)勢(shì)。如果部署得好,JavaScript程序是不會(huì)出現(xiàn)堵塞的,這就是為什么node.js平臺(tái)可以用很少的資源,應(yīng)付大流量訪(fǎng)問(wèn)的原因。
任務(wù)隊(duì)列
如果有大量的異步任務(wù)(實(shí)際情況就是這樣),它們會(huì)在“任務(wù)隊(duì)列”中注冊(cè)大量的事件。這些事件排成隊(duì)列,等候進(jìn)入主線(xià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
Copyright©2021 w3cschool編程獅|閩ICP備15016281號(hào)-3|閩公網(wǎng)安備35020302033924號(hào)
違法和不良信息舉報(bào)電話(huà):173-0602-2364|舉報(bào)郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號(hào)
聯(lián)系方式:
更多建議: