瀏覽器工作原理(22) - JavaScript是如何影響DOM樹構建的?

謝萱徵發表於2020-09-29

上一篇文章我們講了chrome效能皮膚的使用,瞭解了請求過程中的幾個效能指標,這篇文章我們一起來看一下DOM樹是如何生成的,本文主要有兩大塊內容:第一個是解析過程中遇到JavaScript指令碼,DOM解析器是如何處理的?第二個是DOM解析器是如何處理跨站點資源的?

什麼是DOM

從網路傳給渲染引擎的是HTML文件位元組流,並無法執行,需要轉換為渲染引擎能夠識別的DOM樹,在渲染引擎中,DOM有三個層面的作用:

  • 從頁面的視覺來看,DOM是生成頁面的基礎資料結構
  • 從JavaScript指令碼的角度來看,DOM提供給JavaScript指令碼一些API介面,方便JavaScript操作頁面元素
  • 從解析安全的角度來看,DOM是一道攔截器,不符合標準的DOM會提前報錯

總結來說,DOM就是頁面的結構化描述,並可以提供給指令碼一些介面,方便來操作頁面元素,還具備一定的過濾功能

DOM樹如何生成

在渲染引擎內部,負責將HTML文件轉換成DOM結構的模組叫 HTML解析器(HTMLParser) ,那麼HTML解析器是如何工作的呢?

HTML解析器是在文件邊載入的過程中就開始執行解析的,也就是說載入了多少就解析多少

一起來看一下詳細的流程是怎麼樣的,首先是網路程式接收到響應頭,會根據響應頭中的content-type來判斷檔案的型別,比如“text/html”,那麼瀏覽器會判斷這是一個HTML型別的檔案,然後為這個請求建立一個渲染程式,建立好渲染程式,渲染程式和網路程式進行通訊,網路程式把接受到的資料不斷的通過共享資料通道傳送給渲染程式,渲染程式再將資料傳給HTML解析器,解析成DOM。

接下來再一起看看DOM的具體生成流程—位元組流轉換成DOM

在這裡插入圖片描述
從上圖可以看出,位元組流轉換成DOM需要三個階段:

第一個階段,通過分詞器將位元組流轉換為Token

V8執行JavaScript指令碼的時候,會將程式碼先做詞法分析,將JavaScript分解為一個個的Token,這裡HTML的解析也是一樣的,先進行詞法分析,將位元組流轉換為Token,Tag Token文字Token

在這裡插入圖片描述

第二階段和第三階段同步執行,生成DOM節點,將DOM 節點新增到DOM樹中

HTML解析器維護了一個Token棧結構,主要用來計算節點之間的父子關係,第一階段生成的Token按照順序壓入棧中,startTag Token首先壓入棧中,並生成DOM樹,文字節點直接新增到DOM樹不入棧,遇到Endtag Token,則會去棧頂開始找對應的Starttag Token,並彈出Starttag Token,表示該元素已解析完成,直到所有的Token解析完成,示意圖如下:

在這裡插入圖片描述

JavaScript如何影響DOM生成

看一段下面的程式碼:


<html>
<body>
    <div>1</div>
    <script>
    let div1 = document.getElementsByTagName('div')[0]
    div1.innerText = 'example'
    </script>
    <div>test</div>
</body>
</html>

解析這段HTML文件時,遇到script標籤之前都是一樣的過程,遇到script,解析器會判斷這是一段指令碼,HTML解析器會停止當前工作,開始執行指令碼,這時候JavaScript引擎開始工作,執行完指令碼,修改了div1的內容,然後又繼續執行DOM的解析過程

上面的指令碼是新增到了HTML內部,如果指令碼是引入過來的,又該如何執行呢?


<html>
<body>
    <div>1</div>
    <script type="text/javascript" src='foo.js'></script>
    <div>test</div>
</body>
</html>

這裡多了一個指令碼下載的過程,JavaScript的下載會阻塞DOM的解析,如何避免這種情況呢?例如利用CDN來加速資源下載,還可以把指令碼壓縮以減小指令碼大小,還可以通過非同步載入的方式來載入指令碼


 <script async type="text/javascript" src='foo.js'></script>


<script defer type="text/javascript" src='foo.js'></script>

async的方式是非同步載入指令碼,載入完成之後會立即執行,還是有可能阻塞DOM解析。defer標記的指令碼,需要在DOMContentLoaded事件之前執行,也就是HTML解析完成之後再執行,不會阻塞DOM解析

再看一個同時擁有css樣式和JavaScript指令碼的例子


<html>
    <head>
        <style src='theme.css'></style>
    </head>
<body>
    <div>1</div>
    <script>
            let div1 = document.getElementsByTagName('div')[0]
            div1.innerText = 'example' //需要DOM
            div1.style.color = 'red'  //需要CSSOM
        </script>
    <div>test</div>
</body>
</html>

上面的例子中,JavaScript指令碼中操作了CSSDOM,所以在執行指令碼內的樣式操作dom之前,需要先執行完CSS樣式,所以如果程式碼裡引入了外部的CSS,必須是先下載CSS樣式表,再去執行JavaScript

結論

JavaScript指令碼會阻塞HTML解析,而外部CSS樣式表又會阻塞JavaScript指令碼的執行

相關文章