詳細(xì)聊聊JavaScript是如何影響DOM樹(shù)構(gòu)建的
文檔對(duì)象模型 (DOM)
文檔對(duì)象模型 (DOM) 會(huì)將 web 頁(yè)面與到腳本或編程語(yǔ)言連接起來(lái)。DOM模型表示具有邏輯樹(shù)的文檔。樹(shù)的每個(gè)分支的終點(diǎn)都是一個(gè)節(jié)點(diǎn)(node),每個(gè)節(jié)點(diǎn)都包含著對(duì)象(objects)。DOM的方法(methods)允許以編程方式進(jìn)行訪問(wèn)樹(shù),從而改變文檔的結(jié)構(gòu),樣式和內(nèi)容。節(jié)點(diǎn)可以關(guān)聯(lián)上事件處理器,一旦某一事件被觸發(fā)了,那些事件處理器就會(huì)被執(zhí)行。
從網(wǎng)絡(luò)傳給渲染引擎的 HTML 文件字節(jié)流是無(wú)法直接被渲染引擎理解的,所以要將其轉(zhuǎn)化為渲染引擎能夠理解的內(nèi)部結(jié)構(gòu),這個(gè)結(jié)構(gòu)就是 DOM。DOM 提供了對(duì) HTML 文檔結(jié)構(gòu)化的表述。在渲染引擎中,DOM 有三個(gè)層面的作用
- 從頁(yè)面的視角來(lái)看,DOM 是生成頁(yè)面的基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)。
- 從 JavaScript 腳本視角來(lái)看,DOM 提供給 JavaScript 腳本操作的接口,通過(guò)這套接口,JavaScript 可以對(duì) DOM 結(jié)構(gòu)進(jìn)行訪問(wèn),從而改變文檔的結(jié)構(gòu)、樣式和內(nèi)容。
- 從安全視角來(lái)看,DOM 是一道安全防護(hù)線,一些不安全的內(nèi)容在 DOM 解析階段就被拒之門外了。
簡(jiǎn)言之,DOM 是表述 HTML 的內(nèi)部數(shù)據(jù)結(jié)構(gòu),它會(huì)將 Web 頁(yè)面和 JavaScript 腳本連接起來(lái),并過(guò)濾一些不安全的內(nèi)容
DOM 和 JavaScript
DOM 并不是一個(gè)編程語(yǔ)言,但如果沒(méi)有DOM, JavaScript 語(yǔ)言也不會(huì)有任何網(wǎng)頁(yè),XML頁(yè)面以及涉及到的元素的概念或模型。在文檔中的每個(gè)元素— 包括整個(gè)文檔,文檔頭部, 文檔中的表格,表頭,表格中的文本 — 都是文檔所屬于的文檔對(duì)象模型(DOM)的一部分,因此它們可以使用DOM和一個(gè)腳本語(yǔ)言如 JavaScript,來(lái)訪問(wèn)和處理。
起初,JavaScript和DOM是交織在一起的,但它們最終演變成了兩個(gè)獨(dú)立的實(shí)體。JavaScript可以訪問(wèn)和操作存儲(chǔ)在DOM中的內(nèi)容,因此我們可以寫(xiě)成這個(gè)近似的等式:
API (web 或 XML 頁(yè)面) = DOM + JS (腳本語(yǔ)言)
DOM 樹(shù)如何生成
在渲染引擎內(nèi)部,有一個(gè)叫HTML 解析器(HTMLParser)的模塊,它的職責(zé)就是負(fù)責(zé)將 HTML 字節(jié)流轉(zhuǎn)換為 DOM 結(jié)構(gòu)。
HTML 解析器并不是等整個(gè)文檔加載完成之后再解析的,而是隨著 HTML 文檔邊加載邊解析的,是網(wǎng)絡(luò)進(jìn)程加載了多少數(shù)據(jù),HTML 解析器便解析多少數(shù)據(jù)。
流程:網(wǎng)絡(luò)進(jìn)程接收到響應(yīng)頭之后,會(huì)根據(jù)響應(yīng)頭中的 content-type 字段來(lái)判斷文件的類型,比如 content-type 的值是“text/html”,那么瀏覽器就會(huì)判斷這是一個(gè) HTML 類型的文件,根據(jù)這個(gè)判斷選擇相應(yīng)的解析引擎,然后為該請(qǐng)求選擇或者創(chuàng)建一個(gè)渲染進(jìn)程。渲染進(jìn)程準(zhǔn)備好之后,網(wǎng)絡(luò)進(jìn)程和渲染進(jìn)程之間會(huì)建立一個(gè)共享數(shù)據(jù)的管道,網(wǎng)絡(luò)進(jìn)程接收到數(shù)據(jù)后就往這個(gè)管道里面放,而渲染進(jìn)程則從管道的另外一端不斷地讀取數(shù)據(jù),并同時(shí)將讀取的數(shù)據(jù)傳送給 HTML 解析器。
可以把這個(gè)管道想象成一個(gè)“水管”,網(wǎng)絡(luò)進(jìn)程接收到的字節(jié)流像水一樣倒進(jìn)這個(gè)“水管”,而“水管”的另外一端是渲染進(jìn)程的 HTML 解析器,它會(huì)動(dòng)態(tài)接收字節(jié)流,并將其解析為 DOM。
從圖中可以看出,字節(jié)流轉(zhuǎn)換為 DOM 需要三個(gè)階段。
解析 HTML 的三個(gè)階段
第一個(gè)階段,通過(guò)分詞器將字節(jié)流轉(zhuǎn)換為 Token。
解析 HTML 也是一樣的,需要通過(guò)分詞器先將字節(jié)流轉(zhuǎn)換為一個(gè)個(gè) Token,分為 Tag Token 和文本 Token。將 HTML 代碼通過(guò)詞法分析生成的 Token 如下圖所示:
由圖可知,Tag Token 又分 StartTag 和 EndTag。
第二階段是將 Token 解析為 DOM 節(jié)點(diǎn)
HTML 解析器維護(hù)了一個(gè)Token 棧結(jié)構(gòu),該 Token 棧主要用來(lái)計(jì)算節(jié)點(diǎn)之間的父子關(guān)系,在第一個(gè)階段中生成的 Token 會(huì)被按照順序壓到這個(gè)棧中。具體的處理規(guī)則如下所示:
- 如果壓入到棧中的是StartTag Token,HTML 解析器會(huì)為該 Token 創(chuàng)建一個(gè) DOM 節(jié)點(diǎn),然后將該節(jié)點(diǎn)加入到 DOM 樹(shù)中,它的父節(jié)點(diǎn)就是棧中相鄰的那個(gè)元素生成的節(jié)點(diǎn)。
- 如果分詞器解析出來(lái)是文本 Token,那么會(huì)生成一個(gè)文本節(jié)點(diǎn),然后將該節(jié)點(diǎn)加入到 DOM 樹(shù)中,文本 Token 是不需要壓入到棧中,它的父節(jié)點(diǎn)就是當(dāng)前棧頂 Token 所對(duì)應(yīng)的 DOM 節(jié)點(diǎn)。
- 如果分詞器解析出來(lái)的是EndTag 標(biāo)簽,比如是 EndTag div,HTML 解析器會(huì)查看 Token 棧頂?shù)脑厥欠袷?StarTag div,如果是,就將 StartTag div 從棧中彈出,表示該 div 元素解析完成。
通過(guò)分詞器產(chǎn)生的新 Token 就這樣不停地壓棧和出棧,整個(gè)解析過(guò)程就這樣一直持續(xù)下去,直到分詞器將所有字節(jié)流分詞完成。
第三階段是將 DOM 節(jié)點(diǎn)添加到 DOM 樹(shù)中
將創(chuàng)建的 DOM 節(jié)點(diǎn),添加到 document 上,形成 DOM 樹(shù)。
詳解 HTML 解析流程
HTML 解析器開(kāi)始工作時(shí),會(huì)默認(rèn)創(chuàng)建了一個(gè)根為 document 的空 DOM 結(jié)構(gòu),同時(shí)會(huì)將一個(gè) StartTag document 的 Token 壓入棧底。然后經(jīng)過(guò)分詞器解析出來(lái)的第一個(gè) StartTag html Token 會(huì)被壓入到棧中,并創(chuàng)建一個(gè) html 的 DOM 節(jié)點(diǎn),添加到 document 上,如下圖所示
然后按照同樣的流程解析出來(lái) StartTag body 和 StartTag div,其 Token 棧和 DOM 的狀態(tài)如下圖所示:
接下來(lái)解析出來(lái)的是第一個(gè) div 的文本 Token,渲染引擎會(huì)為該 Token 創(chuàng)建一個(gè)文本節(jié)點(diǎn),并將該 Token 添加到 DOM 中,它的父節(jié)點(diǎn)就是當(dāng)前 Token 棧頂元素對(duì)應(yīng)的節(jié)點(diǎn),如下圖所示:
再接下來(lái),分詞器解析出來(lái)第一個(gè) EndTag div,這時(shí)候 HTML 解析器會(huì)去判斷當(dāng)前棧頂?shù)脑厥欠袷?StartTag div,如果是則從棧頂彈出 StartTag div,如下圖所示
按照同樣的規(guī)則,一路解析,最終結(jié)果如下圖所示:
通過(guò)上面的介紹,相信你已經(jīng)清楚 DOM 是怎么生成的了。不過(guò)在實(shí)際生產(chǎn)環(huán)境中,HTML 源文件中既包含 CSS 和 JavaScript,又包含圖片、音頻、視頻等文件,所以處理過(guò)程遠(yuǎn)比上面這個(gè) Demo 復(fù)雜。不過(guò)理解了這個(gè)簡(jiǎn)單的 Demo 生成過(guò)程,我們就可以往下分析更加復(fù)雜的場(chǎng)景了。
JavaScript 是如何影響 DOM 生成的
如果頁(yè)面中含有一段 JavaScript 腳本,或者引入了腳本文件,則這段腳本的解析過(guò)程就與上面的過(guò)程有點(diǎn)不一樣了。
script標(biāo)簽之前,所有的解析流程還是和之前介紹的一樣,但是解析到script標(biāo)簽時(shí),渲染引擎判斷這是一段腳本,此時(shí) HTML 解析器就會(huì)暫停 DOM 的解析,JavaScript 引擎介入,因?yàn)?JavaScript 腳本可能要修改當(dāng)前已經(jīng)生成的 DOM 結(jié)構(gòu)。
如果腳本是通過(guò) JavaScript 文件加載的,則需要先下載這段 JavaScript 代碼。這里需要重點(diǎn)關(guān)注下載環(huán)境,因?yàn)镴avaScript 文件的下載過(guò)程會(huì)阻塞 DOM 解析,而通常下載又是非常耗時(shí)的,會(huì)受到網(wǎng)絡(luò)環(huán)境、JavaScript 文件大小等因素的影響。
如果腳本是直接內(nèi)嵌的 JavaScript 腳本,則直接執(zhí)行。
如果 JavaScript 腳本修改了 DOM 中的 div 中的內(nèi)容,所以執(zhí)行這段腳本之后,已經(jīng)解析過(guò)的 div 節(jié)點(diǎn)內(nèi)容也會(huì)被修改。腳本執(zhí)行完成之后,HTML 解析器恢復(fù)解析過(guò)程,繼續(xù)解析后續(xù)的內(nèi)容,直至生成最終的 DOM。
還有一種情況則是,如果 JavaScript 代碼出現(xiàn)了,修改頁(yè)面 CSS 樣式的語(yǔ)句,用來(lái)操縱 CSSOM ,所以在執(zhí)行 JavaScript 之前,需要先解析 JavaScript 語(yǔ)句之上所有的 CSS 樣式。所以如果代碼里引用了外部的 CSS 文件,那么在執(zhí)行 JavaScript 之前,還需要等待外部的 CSS 文件下載完成,并解析生成 CSSOM 對(duì)象之后,才能執(zhí)行 JavaScript 腳本。
而 JavaScript 引擎在解析 JavaScript 代碼之前,是不知道 JavaScript 是否操縱了 CSSOM 的,所以渲染引擎在遇到 JavaScript 腳本時(shí),不管該腳本是否操縱了 CSSOM,都會(huì)執(zhí)行 CSS 文件下載,解析操作,再執(zhí)行 JavaScript 腳本。所以說(shuō) JavaScript 腳本是依賴樣式表的。
通過(guò)上面的分析,我們知道了 JavaScript 會(huì)阻塞 DOM 生成,而樣式文件又會(huì)阻塞 JavaScript 的執(zhí)行,所以在實(shí)際的工程中需要重點(diǎn)關(guān)注 JavaScript 文件和樣式表文件,使用不當(dāng)會(huì)影響到頁(yè)面性能的。
解析過(guò)程中的優(yōu)化
為防止頁(yè)面阻塞,Chrome 瀏覽器做了很多優(yōu)化,其中一個(gè)主要的優(yōu)化是預(yù)解析操作。當(dāng)渲染引擎收到字節(jié)流之后,會(huì)開(kāi)啟一個(gè)預(yù)解析線程,用來(lái)分析 HTML 文件中包含的 JavaScript、CSS 等相關(guān)文件,解析到相關(guān)文件之后,預(yù)解析線程會(huì)提前下載這些文件。
再回到 DOM 解析上,我們知道引入 JavaScript 線程會(huì)阻塞 DOM,不過(guò)也有一些相關(guān)的策略來(lái)規(guī)避,比如使用 CDN 來(lái)加速 JavaScript 文件的加載,壓縮 JavaScript 文件的體積。另外,如果 JavaScript 文件中沒(méi)有操作 DOM 相關(guān)代碼,就可以將該 JavaScript 腳本設(shè)置為異步加載,通過(guò) async 或 defer 來(lái)標(biāo)記代碼,使用方式如下所示:
<script async type="text/javascript" src='foo.js'></script>
<script defer type="text/javascript" src='foo.js'></script>
async 和 defer 雖然都是異步的,不過(guò)還有一些差異,使用 async 標(biāo)志的腳本文件一旦加載完成,會(huì)立即執(zhí)行;而使用了 defer 標(biāo)記的腳本文件,需要在 DOMContentLoaded 事件之前執(zhí)行。
總結(jié)
首先我們介紹了 DOM 是如何生成的,然后又基于 DOM 的生成過(guò)程分析了 JavaScript 是如何影響到 DOM 生成的。也談到 CSS 和 JavaScript 都會(huì)影響到 DOM 的生成。
DOM生成的過(guò)程
解析 HTML 需要通過(guò)分詞器先將字節(jié)流轉(zhuǎn)換為 Token。
如果壓入到棧中的是StartTag Token,HTML 解析器會(huì)為該 Token 創(chuàng)建一個(gè) DOM 節(jié)點(diǎn),然后將該節(jié)點(diǎn)加入到 DOM 樹(shù)中。如果分詞器解析出來(lái)是文本 Token,那么會(huì)生成一個(gè)文本節(jié)點(diǎn),然后將該節(jié)點(diǎn)加入到 DOM 樹(shù)中。如果分詞器解析出來(lái)的是EndTag 標(biāo)簽,HTML 解析器會(huì)查看 Token 棧頂?shù)脑厥欠袷?StarTag div,如果是,就將 StartTag div 從棧中彈出,表示該 div 元素解析完成。
通過(guò)分詞器產(chǎn)生的新 Token 就這樣不停地壓棧和出棧,整個(gè)解析過(guò)程就這樣一直持續(xù)下去,直到分詞器將所有字節(jié)流分詞完成。
在解析過(guò)程中如果遇到 JavaScript 代碼,則停止 HTML 解析,如果js通過(guò)腳本加載的則先下載該腳本再執(zhí)行,再執(zhí)行之前 CSS 也會(huì)被解析生成 CSSOM。經(jīng)此過(guò)程直至整個(gè) DOM 構(gòu)建完成。
到此這篇關(guān)于JavaScript是如何影響DOM樹(shù)構(gòu)建的文章就介紹到這了,更多相關(guān)JavaScript DOM樹(shù)構(gòu)建內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
仿163填寫(xiě)郵件地址自動(dòng)顯示下拉(無(wú)優(yōu)化)
本框內(nèi)填個(gè)1,這些值都寫(xiě)在隱藏域了。代碼里可以看到,用戶輸入包含在里面的時(shí)候,可以按ENTER鍵選中.2008-11-11JavaScript 轉(zhuǎn)義字符JSON parse錯(cuò)誤研究
這篇文章主要為大家介紹了JavaScript 轉(zhuǎn)義字符JSON parse錯(cuò)誤研究,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10關(guān)于promise.all()的使用及說(shuō)明
這篇文章主要介紹了關(guān)于promise.all()的使用及說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-04-04javascript數(shù)組的定義及操作實(shí)例
在文章里小編給大家整理的是關(guān)于javascript數(shù)組的定義及操作的相關(guān)知識(shí)點(diǎn),需要的朋友們學(xué)習(xí)下。2019-11-11JavaScript中為什么null==0為false而null大于=0為true(個(gè)人研究)
今天閑來(lái)沒(méi)啥事,研究了一下有關(guān)“null”和“0”的關(guān)系。希望大家看完了能有所收獲,在此與大家分享下,希望也可以受益匪淺2013-09-09JS數(shù)據(jù)結(jié)構(gòu)之隊(duì)列結(jié)構(gòu)詳解
這篇文章主要為大家詳細(xì)介紹了JavaScript數(shù)據(jù)結(jié)構(gòu)與算法中的隊(duì)列結(jié)構(gòu),文中通過(guò)簡(jiǎn)單的示例介紹了隊(duì)列結(jié)構(gòu)的原理與實(shí)現(xiàn),需要的可以參考一下2022-11-11