JavaScript 是瀏覽器的內(nèi)置腳本語言呕缭。也就是說,瀏覽器內(nèi)置了 JavaScript 引擎筒愚,并且提供各種接口赴蝇,讓 JavaScript 腳本可以控制瀏覽器的各種功能。一旦網(wǎng)頁內(nèi)嵌了 JavaScript 腳本锨能,瀏覽器加載網(wǎng)頁扯再,就會去執(zhí)行腳本,從而達到操作瀏覽器的目的址遇,實現(xiàn)網(wǎng)頁的各種動態(tài)效果熄阻。
代碼嵌入網(wǎng)頁的方法
網(wǎng)頁中嵌入 JavaScript 代碼,主要有三種方法倔约。
-
<script>
元素直接嵌入代碼秃殉。 -
<script>
標簽加載外部腳本 - 事件屬性
- URL 協(xié)議
script 元素嵌入代碼
<script>
元素內(nèi)部可以直接寫 JavaScript 代碼。
<script>
var x = 1 + 5;
console.log(x);
</script>
<script>
標簽有一個type
屬性浸剩,用來指定腳本類型钾军。對 JavaScript 腳本來說,type
屬性可以設(shè)為兩種值绢要。
-
text/javascript
:這是默認值吏恭,也是歷史上一貫設(shè)定的值。如果你省略type
屬性重罪,默認就是這個值樱哼。對于老式瀏覽器哀九,設(shè)為這個值比較好。 -
application/javascript
:對于較新的瀏覽器搅幅,建議設(shè)為這個值阅束。
<script type="application/javascript">
console.log('Hello World');
</script>
由于<script>
標簽?zāi)J就是 JavaScript 代碼。所以茄唐,嵌入 JavaScript 腳本時息裸,type
屬性可以省略。
如果type
屬性的值沪编,瀏覽器不認識呼盆,那么它不會執(zhí)行其中的代碼。利用這一點漾抬,可以在<script>
標簽之中嵌入任意的文本內(nèi)容宿亡,只要加上一個瀏覽器不認識的type
屬性即可。
<script id="mydata" type="x-custom-data">
console.log('Hello World');
</script>
上面的代碼纳令,瀏覽器不會執(zhí)行,也不會顯示它的內(nèi)容克胳,因為不認識它的type
屬性平绩。但是,這個<script>
節(jié)點依然存在于 DOM 之中漠另,可以使用<script>
節(jié)點的text
屬性讀出它的內(nèi)容捏雌。
document.getElementById('mydata').text
// console.log('Hello World');
script 元素加載外部腳本
<script>
標簽也可以指定加載外部的腳本文件。
<script src="https://www.example.com/script.js"></script>
如果腳本文件使用了非英語字符笆搓,還應(yīng)該注明字符的編碼性湿。
<script charset="utf-8" src="https://www.example.com/script.js"></script>
所加載的腳本必須是純的 JavaScript 代碼,不能有HTML
代碼和<script>
標簽满败。
加載外部腳本和直接添加代碼塊肤频,這兩種方法不能混用。下面代碼的console.log
語句直接被忽略算墨。
<script charset="utf-8" src="example.js">
console.log('Hello World!');
</script>
為了防止攻擊者篡改外部腳本宵荒,script
標簽允許設(shè)置一個integrity
屬性,寫入該外部腳本的 Hash 簽名净嘀,用來驗證腳本的一致性报咳。
<script src="/assets/application.js"
integrity="sha256-TvVUHzSfftWg1rcfL6TIJ0XKEGrgLyEq6lEpcmrG9qs=">
</script>
上面代碼中,script
標簽有一個integrity
屬性挖藏,指定了外部腳本/assets/application.js
的 SHA256 簽名暑刃。一旦有人改了這個腳本,導(dǎo)致 SHA256 簽名不匹配膜眠,瀏覽器就會拒絕加載岩臣。
事件屬性
網(wǎng)頁元素的事件屬性(比如onclick
和onmouseover
)溜嗜,可以寫入 JavaScript 代碼。當(dāng)指定事件發(fā)生時婿脸,就會調(diào)用這些代碼粱胜。
<button id="myBtn" onclick="console.log(this.id)">點擊</button>
上面的事件屬性代碼只有一個語句。如果有多個語句狐树,使用分號分隔即可焙压。
URL 協(xié)議
URL 支持javascript:
協(xié)議,即在 URL 的位置寫入代碼抑钟,使用這個 URL 的時候就會執(zhí)行 JavaScript 代碼涯曲。
<a href="javascript:console.log('Hello')">點擊</a>
瀏覽器的地址欄也可以執(zhí)行javascript:
協(xié)議。將javascript:console.log('Hello')
放入地址欄在塔,按回車鍵也會執(zhí)行這段代碼幻件。
如果 JavaScript 代碼返回一個字符串,瀏覽器就會新建一個文檔蛔溃,展示這個字符串的內(nèi)容绰沥,原有文檔的內(nèi)容都會消失。
<a href="javascript: new Date().toLocaleTimeString();">點擊</a>
上面代碼中贺待,用戶點擊鏈接以后徽曲,會打開一個新文檔,里面有當(dāng)前時間麸塞。
如果返回的不是字符串秃臣,那么瀏覽器不會新建文檔,也不會跳轉(zhuǎn)哪工。
<a href="javascript: console.log(new Date().toLocaleTimeString())">點擊</a>
上面代碼中奥此,用戶點擊鏈接后,網(wǎng)頁不會跳轉(zhuǎn)雁比,只會在控制臺顯示當(dāng)前時間稚虎。
javascript:
協(xié)議的常見用途是書簽?zāi)_本 Bookmarklet。由于瀏覽器的書簽保存的是一個網(wǎng)址章贞,所以javascript:
網(wǎng)址也可以保存在里面祥绞,用戶選擇這個書簽的時候,就會在當(dāng)前頁面執(zhí)行這個腳本鸭限。為了防止書簽替換掉當(dāng)前文檔蜕径,可以在腳本前加上void
,或者在腳本最后加上void 0
败京。
<a href="javascript: void new Date().toLocaleTimeString();">點擊</a>
<a href="javascript: new Date().toLocaleTimeString();void 0;">點擊</a>
上面這兩種寫法兜喻,點擊鏈接后,執(zhí)行代碼都不會網(wǎng)頁跳轉(zhuǎn)赡麦。
script 元素
工作原理
瀏覽器加載 JavaScript 腳本朴皆,主要通過<script>
元素完成帕识。正常的網(wǎng)頁加載流程是這樣的。
- 瀏覽器一邊下載 HTML 網(wǎng)頁遂铡,一邊開始解析肮疗。也就是說,不等到下載完扒接,就開始解析伪货。
- 解析過程中,瀏覽器發(fā)現(xiàn)
<script>
元素钾怔,就暫停解析碱呼,把網(wǎng)頁渲染的控制權(quán)轉(zhuǎn)交給 JavaScript 引擎。 - 如果
<script>
元素引用了外部腳本宗侦,就下載該腳本再執(zhí)行愚臀,否則就直接執(zhí)行代碼。 - JavaScript 引擎執(zhí)行完畢矾利,控制權(quán)交還渲染引擎姑裂,恢復(fù)往下解析 HTML 網(wǎng)頁。
加載外部腳本時男旗,瀏覽器會暫停頁面渲染炭分,等待腳本下載并執(zhí)行完成后,再繼續(xù)渲染剑肯。原因是 JavaScript 代碼可以修改 DOM,所以必須把控制權(quán)讓給它观堂,否則會導(dǎo)致復(fù)雜的線程競賽的問題让网。
如果外部腳本加載時間很長(一直無法完成下載),那么瀏覽器就會一直等待腳本下載完成师痕,造成網(wǎng)頁長時間失去響應(yīng)溃睹,瀏覽器就會呈現(xiàn)“假死”狀態(tài),這被稱為“阻塞效應(yīng)”胰坟。
為了避免這種情況因篇,較好的做法是將<script>
標簽都放在頁面底部,而不是頭部笔横。這樣即使遇到腳本失去響應(yīng)竞滓,網(wǎng)頁主體的渲染也已經(jīng)完成了,用戶至少可以看到內(nèi)容吹缔,而不是面對一張空白的頁面商佑。如果某些腳本代碼非常重要,一定要放在頁面頭部的話厢塘,最好直接將代碼寫入頁面茶没,而不是連接外部腳本文件肌幽,這樣能縮短加載時間。
腳本文件都放在網(wǎng)頁尾部加載抓半,還有一個好處喂急。因為在 DOM 結(jié)構(gòu)生成之前就調(diào)用 DOM 節(jié)點,JavaScript 會報錯笛求,如果腳本都在網(wǎng)頁尾部加載廊移,就不存在這個問題,因為這時 DOM 肯定已經(jīng)生成了涣易。
<head>
<script>
console.log(document.body.innerHTML);
</script>
</head>
<body>
</body>
上面代碼執(zhí)行時會報錯画机,因為此時document.body
元素還未生成。
一種解決方法是設(shè)定DOMContentLoaded
事件的回調(diào)函數(shù)新症。
<head>
<script>
document.addEventListener(
'DOMContentLoaded',
function (event) {
console.log(document.body.innerHTML);
}
);
</script>
</head>
上面代碼中步氏,指定DOMContentLoaded
事件發(fā)生后,才開始執(zhí)行相關(guān)代碼徒爹。DOMContentLoaded
事件只有在 DOM 結(jié)構(gòu)生成之后才會觸發(fā)荚醒。
另一種解決方法是,使用<script>
標簽的onload
屬性隆嗅。當(dāng)<script>
標簽指定的外部腳本文件下載和解析完成界阁,會觸發(fā)一個load
事件,可以把所需執(zhí)行的代碼胖喳,放在這個事件的回調(diào)函數(shù)里面泡躯。
<script src="jquery.min.js" onload="console.log(document.body.innerHTML)">
</script>
但是,如果將腳本放在頁面底部丽焊,就可以完全按照正常的方式寫较剃,上面兩種方式都不需要。
<body>
<!-- 其他代碼 -->
<script>
console.log(document.body.innerHTML);
</script>
</body>
如果有多個script
標簽技健,比如下面這樣写穴。
<script src="a.js"></script>
<script src="b.js"></script>
瀏覽器會同時并行下載a.js
和b.js
,但是雌贱,執(zhí)行時會保證先執(zhí)行a.js
啊送,然后再執(zhí)行b.js
,即使后者先下載完成欣孤,也是如此馋没。也就是說,腳本的執(zhí)行順序由它們在頁面中的出現(xiàn)順序決定导街,這是為了保證腳本之間的依賴關(guān)系不受到破壞披泪。當(dāng)然,加載這兩個腳本都會產(chǎn)生“阻塞效應(yīng)”搬瑰,必須等到它們都加載完成款票,瀏覽器才會繼續(xù)頁面渲染控硼。
解析和執(zhí)行 CSS,也會產(chǎn)生阻塞艾少。Firefox 瀏覽器會等到腳本前面的所有樣式表卡乾,都下載并解析完,再執(zhí)行腳本缚够;Webkit則是一旦發(fā)現(xiàn)腳本引用了樣式幔妨,就會暫停執(zhí)行腳本,等到樣式表下載并解析完谍椅,再恢復(fù)執(zhí)行误堡。
此外,對于來自同一個域名的資源雏吭,比如腳本文件锁施、樣式表文件、圖片文件等杖们,瀏覽器一般有限制悉抵,同時最多下載6~20個資源,即最多同時打開的 TCP 連接有限制摘完,這是為了防止對服務(wù)器造成太大壓力姥饰。如果是來自不同域名的資源,就沒有這個限制孝治。所以列粪,通常把靜態(tài)文件放在不同的域名之下,以加快下載速度谈飒。
defer 屬性
為了解決腳本文件下載阻塞網(wǎng)頁渲染的問題篱竭,一個方法是對<script>
元素加入defer
屬性。它的作用是延遲腳本的執(zhí)行步绸,等到 DOM 加載生成后,再執(zhí)行腳本吃媒。
<script src="a.js" defer></script>
<script src="b.js" defer></script>
上面代碼中瓤介,只有等到 DOM 加載完成后,才會執(zhí)行a.js
和b.js
赘那。
defer
屬性的運行流程如下刑桑。
- 瀏覽器開始解析 HTML 網(wǎng)頁。
- 解析過程中募舟,發(fā)現(xiàn)帶有
defer
屬性的<script>
元素祠斧。 - 瀏覽器繼續(xù)往下解析 HTML 網(wǎng)頁,同時并行下載
<script>
元素加載的外部腳本拱礁。 - 瀏覽器完成解析 HTML 網(wǎng)頁琢锋,此時再回過頭執(zhí)行已經(jīng)下載完成的腳本辕漂。
有了defer
屬性,瀏覽器下載腳本文件的時候吴超,不會阻塞頁面渲染钉嘹。下載的腳本文件在DOMContentLoaded
事件觸發(fā)前執(zhí)行(即剛剛讀取完</html>
標簽),而且可以保證執(zhí)行順序就是它們在頁面上出現(xiàn)的順序鲸阻。
對于內(nèi)置而不是加載外部腳本的script
標簽跋涣,以及動態(tài)生成的script
標簽,defer
屬性不起作用鸟悴。另外陈辱,使用defer
加載的外部腳本不應(yīng)該使用document.write
方法。
async 屬性
解決“阻塞效應(yīng)”的另一個方法是對<script>
元素加入async
屬性细诸。
<script src="a.js" async></script>
<script src="b.js" async></script>
async
屬性的作用是沛贪,使用另一個進程下載腳本,下載時不會阻塞渲染揍堰。
- 瀏覽器開始解析 HTML 網(wǎng)頁鹏浅。
- 解析過程中,發(fā)現(xiàn)帶有
async
屬性的script
標簽屏歹。 - 瀏覽器繼續(xù)往下解析 HTML 網(wǎng)頁隐砸,同時并行下載
<script>
標簽中的外部腳本。 - 腳本下載完成蝙眶,瀏覽器暫停解析 HTML 網(wǎng)頁季希,開始執(zhí)行下載的腳本。
- 腳本執(zhí)行完畢幽纷,瀏覽器恢復(fù)解析 HTML 網(wǎng)頁式塌。
async
屬性可以保證腳本下載的同時,瀏覽器繼續(xù)渲染友浸。需要注意的是峰尝,一旦采用這個屬性,就無法保證腳本的執(zhí)行順序收恢。哪個腳本先下載結(jié)束武学,就先執(zhí)行那個腳本。另外伦意,使用async
屬性的腳本文件里面的代碼火窒,不應(yīng)該使用document.write
方法。
defer
屬性和async
屬性到底應(yīng)該使用哪一個驮肉?
一般來說熏矿,如果腳本之間沒有依賴關(guān)系,就使用async
屬性,如果腳本之間有依賴關(guān)系票编,就使用defer
屬性褪储。如果同時使用async
和defer
屬性,后者不起作用栏妖,瀏覽器行為由async
屬性決定乱豆。
腳本的動態(tài)加載
<script>
元素還可以動態(tài)生成,生成后再插入頁面吊趾,從而實現(xiàn)腳本的動態(tài)加載宛裕。
['a.js', 'b.js'].forEach(function(src) {
var script = document.createElement('script');
script.src = src;
document.head.appendChild(script);
});
這種方法的好處是,動態(tài)生成的script
標簽不會阻塞頁面渲染论泛,也就不會造成瀏覽器假死揩尸。但是問題在于,這種方法無法保證腳本的執(zhí)行順序屁奏,哪個腳本文件先下載完成岩榆,就先執(zhí)行哪個。
如果想避免這個問題坟瓢,可以設(shè)置async
屬性為false
勇边。
['a.js', 'b.js'].forEach(function(src) {
var script = document.createElement('script');
script.src = src;
script.async = false;
document.head.appendChild(script);
});
上面的代碼不會阻塞頁面渲染,而且可以保證b.js
在a.js
后面執(zhí)行折联。不過需要注意的是粒褒,在這段代碼后面加載的腳本文件,會因此都等待b.js
執(zhí)行完成后再執(zhí)行诚镰。
如果想為動態(tài)加載的腳本指定回調(diào)函數(shù)奕坟,可以使用下面的寫法。
function loadScript(src, done) {
var js = document.createElement('script');
js.src = src;
js.onload = function() {
done();
};
js.onerror = function() {
done(new Error('Failed to load script ' + src));
};
document.head.appendChild(js);
}
加載使用的協(xié)議
如果不指定協(xié)議清笨,瀏覽器默認采用 HTTP 協(xié)議下載月杉。
<script src="example.js"></script>
上面的example.js
默認就是采用 HTTP 協(xié)議下載,如果要采用 HTTPS 協(xié)議下載抠艾,必需寫明苛萎。
<script src="https://example.js"></script>
但是有時我們會希望,根據(jù)頁面本身的協(xié)議來決定加載協(xié)議检号,這時可以采用下面的寫法首懈。
<script src="http://example.js"></script>
瀏覽器的組成
瀏覽器的核心是兩部分:渲染引擎和 JavaScript 解釋器(又稱 JavaScript 引擎)。
渲染引擎
渲染引擎的主要作用是谨敛,將網(wǎng)頁代碼渲染為用戶視覺可以感知的平面文檔坡锡。
不同的瀏覽器有不同的渲染引擎宜猜。
- Firefox:Gecko 引擎
- Safari:WebKit 引擎
- Chrome:Blink 引擎
- IE: Trident 引擎
- Edge: EdgeHTML 引擎
渲染引擎處理網(wǎng)頁,通常分成四個階段缴渊。
- 解析代碼:HTML 代碼解析為 DOM,CSS 代碼解析為 CSSOM(CSS Object Model)炊甲。
- 對象合成:將 DOM 和 CSSOM 合成一棵渲染樹(
render tree
)泥彤。 - 布局:計算出渲染樹的布局(
layout
)。 - 繪制:將渲染樹繪制到屏幕卿啡。
以上四步并非嚴格按順序執(zhí)行吟吝,往往第一步還沒完成,第二步和第三步就已經(jīng)開始了颈娜。所以剑逃,會看到這種情況:網(wǎng)頁的 HTML 代碼還沒下載完,但瀏覽器已經(jīng)顯示出內(nèi)容了官辽。
重流和重繪
渲染樹轉(zhuǎn)換為網(wǎng)頁布局蛹磺,稱為“布局流”(flow
);布局顯示到頁面的這個過程同仆,稱為“繪制”(paint
)萤捆。它們都具有阻塞效應(yīng),并且會耗費很多時間和計算資源俗批。
頁面生成以后俗或,腳本操作和樣式表操作,都會觸發(fā)“重流”和“重繪”岁忘。用戶的互動也會觸發(fā)重流和重繪辛慰,比如設(shè)置了鼠標懸停(a:hover
)效果、頁面滾動臭觉、在輸入框中輸入文本昆雀、改變窗口大小等等。
重流和重繪并不一定一起發(fā)生蝠筑,重流必然導(dǎo)致重繪狞膘,重繪不一定需要重流。比如改變元素顏色什乙,只會導(dǎo)致重繪挽封,而不會導(dǎo)致重流;改變元素的布局臣镣,則會導(dǎo)致重繪和重流辅愿。
大多數(shù)情況下,瀏覽器會智能判斷忆某,將重流和重繪只限制到相關(guān)的子樹上面点待,最小化所耗費的代價,而不會全局重新生成網(wǎng)頁弃舒。
作為開發(fā)者癞埠,應(yīng)該盡量設(shè)法降低重繪的次數(shù)和成本状原。比如,盡量不要變動高層的 DOM 元素苗踪,而以底層 DOM 元素的變動代替颠区;再比如,重繪table
布局和flex
布局通铲,開銷都會比較大毕莱。
var foo = document.getElementById('foobar');
foo.style.color = 'blue';
foo.style.marginTop = '30px';
上面的代碼只會導(dǎo)致一次重繪,因為瀏覽器會累積 DOM 變動颅夺,然后一次性執(zhí)行朋截。
下面是一些優(yōu)化技巧。
- 讀取 DOM 或者寫入 DOM碗啄,盡量寫在一起质和,不要混雜。不要讀取一個 DOM 節(jié)點稚字,然后立刻寫入饲宿,接著再讀取一個 DOM 節(jié)點。
- 緩存 DOM 信息胆描。
- 不要一項一項地改變樣式瘫想,而是使用 CSS class 一次性改變樣式。
- 使用
documentFragment
操作 DOM - 動畫使用
absolute
定位或fixed
定位昌讲,這樣可以減少對其他元素的影響国夜。 - 只在必要時才顯示隱藏元素。
- 使用
window.requestAnimationFrame()
短绸,因為它可以把代碼推遲到下一次重流時執(zhí)行车吹,而不是立即要求頁面重流。 - 使用虛擬 DOM(virtual DOM)庫醋闭。
下面是一個window.requestAnimationFrame()
對比效果的例子窄驹。
// 重繪代價高
function doubleHeight(element) {
var currentHeight = element.clientHeight;
element.style.height = (currentHeight * 2) + 'px';
}
all_my_elements.forEach(doubleHeight);
// 重繪代價低
function doubleHeight(element) {
var currentHeight = element.clientHeight;
window.requestAnimationFrame(function () {
element.style.height = (currentHeight * 2) + 'px';
});
}
all_my_elements.forEach(doubleHeight);
上面的第一段代碼,每讀一次 DOM证逻,就寫入新的值乐埠,會造成不停的重排和重流。第二段代碼把所有的寫操作囚企,都累積在一起丈咐,從而 DOM 代碼變動的代價就最小化了。
JavaScript 引擎
JavaScript 引擎的主要作用是龙宏,讀取網(wǎng)頁中的 JavaScript 代碼棵逊,對其處理后運行。
JavaScript 是一種解釋型語言银酗,也就是說辆影,它不需要編譯掩浙,由解釋器實時運行。這樣的好處是運行和修改都比較方便秸歧,刷新頁面就可以重新解釋;缺點是每次運行都要調(diào)用解釋器衅澈,系統(tǒng)開銷較大键菱,運行速度慢于編譯型語言。
為了提高運行速度今布,目前的瀏覽器都將 JavaScript 進行一定程度的編譯经备,生成類似字節(jié)碼(bytecode
)的中間代碼,以提高運行速度部默。
早期侵蒙,瀏覽器內(nèi)部對 JavaScript 的處理過程如下:
- 讀取代碼,進行詞法分析(
Lexical analysis
)傅蹂,將代碼分解成詞元(token
)纷闺。 - 對詞元進行語法分析(
parsing
),將代碼整理成“語法樹”(syntax tree
)份蝴。 - 使用“翻譯器”(
translator
)犁功,將代碼轉(zhuǎn)為字節(jié)碼(bytecode
)。 - 使用“字節(jié)碼解釋器”(
bytecode interpreter
)婚夫,將字節(jié)碼轉(zhuǎn)為機器碼浸卦。
逐行解釋將字節(jié)碼轉(zhuǎn)為機器碼,是很低效的案糙。為了提高運行速度限嫌,現(xiàn)代瀏覽器改為采用“即時編譯”(Just In Time compiler,縮寫 JIT)时捌,即字節(jié)碼只在運行時編譯怒医,用到哪一行就編譯哪一行,并且把編譯結(jié)果緩存(inline cache
)匣椰。通常裆熙,一個程序被經(jīng)常用到的,只是其中一小部分代碼禽笑,有了緩存的編譯結(jié)果入录,整個程序的運行速度就會顯著提升。
字節(jié)碼不能直接運行佳镜,而是運行在一個虛擬機之上僚稿,一般也把虛擬機稱為 JavaScript 引擎。并非所有的 JavaScript 虛擬機運行時都有字節(jié)碼蟀伸,有的 JavaScript 虛擬機基于源碼蚀同,即只要有可能缅刽,就通過 JIT(just in time)編譯器直接把源碼編譯成機器碼運行,省略字節(jié)碼步驟蠢络。這一點與其他采用虛擬機(比如 Java)的語言不盡相同衰猛。這樣做的目的,是為了盡可能地優(yōu)化代碼刹孔、提高性能啡省。下面是目前最常見的一些 JavaScript 虛擬機:
- Chakra(Microsoft Internet Explorer)
- Nitro/JavaScript Core(Safari)
- Carakan (Opera)
- SpiderMonkey (Firefox)
- V8(Chrome, Chromium)