详解浏览器渲染原理及流程
今天來分享一下瀏覽器的渲染原理及流程。
前言
??先來看看 Chrome 瀏覽器的多進程架構:
??通常,我們打包出來的 HTML、CSS、JavaScript 等文件,經過瀏覽器運行之后就會顯示出頁面,這個過程就是瀏覽器的渲染進程來操作實現的,渲染進程的主要任務就是將靜態資源轉化為可視化界面:
??對于中間的瀏覽器,它就是一個黑盒,下面就來看看這個黑盒是如何將靜態資源轉化為前端界面的。由于渲染機制比較復雜,所以渲染模塊在執行過程中會被劃分為很多子階段,輸入的靜態資源經過這些子階段,最后輸出頁面。我們將一個處理流程稱為渲染流水線,其大致流程如下圖所示:
??這里主要包含五個過程:
?● DOM樹構建:渲染引擎使用HTML解析器(調用XML解析器)解析HTML文檔,將各個HTML元素逐個轉化成DOM節點,從而生成DOM樹;
?● CSSOM樹構建:CSS解析器解析CSS,并將其轉化為CSS對象,將這些CSS對象組裝起來,構建CSSOM樹;
?● 渲染樹構建:DOM 樹和 CSSOM 樹都構建完成以后,瀏覽器會根據這兩棵樹構建出一棵渲染樹;
?● 頁面布局:渲染樹構建完畢之后,元素的位置關系以及需要應用的樣式就確定了,這時瀏覽器會計算出所有元素的大小和絕對位置;
?● 頁面繪制:頁面布局完成之后,瀏覽器會將根據處理出來的結果,把每一個頁面圖層轉換為像素,并對所有的媒體文件進行解碼。
??對于這五個流程,每一階段都有對應的產物:DOM樹、CSSOM樹、渲染樹、盒模型、界面。
??下圖為渲染引擎工作流程中各個步驟所對應的模塊:
??從圖中可以看出,渲染引擎主要包含的模塊有:
?● HTML 解析器:解析HTML文檔,主要作用是將HTML文檔轉換成DOM樹;
?● CSS 解析器:將DOM中的各個元素對象進行計算,獲取樣式信息,用于渲染樹的構建;
?● JavaScript 解釋器:使用JavaScript可以修改網頁的內容、CSS規則等。JavaScript解釋器能夠解釋JavaScript代碼,并通過DOM接口和CSSOM接口來修改網頁內容、樣式規則,從而改變渲染結果;
?● 頁面布局:DOM創建之后,渲染引擎將其中的元素對象與樣式規則進行結合,可以得到渲染樹。布局則是針對渲染樹,計算其各個元素的大小、位置等布局信息。
?● 頁面繪制:使用圖形庫將布局計算后的渲染樹繪制成可視化的圖像結果。
DOM樹構建
??在說構建DOM樹之前,我們需要知道,為什么要構建DOM樹呢? 這是因為,瀏覽器無法直接理解和使用 HTML,所以需要將HTML轉化為瀏覽器能夠理解的結構——DOM樹。
??了解過數據結構的小伙伴對于樹結構應該不陌生,樹是由結點或頂點和邊組成的且不存在著任何環的一種數據結構。一棵非空的樹包括一個根結點,還有多個附加結點,所有結點構成一個多級分層結構。下面通過一張圖來看看什么是樹結構:
??對于上面的三個結構,前兩個都是樹,他們都只有唯一的根節點,而且不存在環結構。而第三個存在環,所以就不是一個樹結構。
??在頁面中,每個HTML標簽都會被瀏覽器解析成文檔對象。HTML本質上就是一個嵌套結構,在解析時會把每個文檔對象用一個樹形結構組織起來,所有的文檔對象都會掛在document上,這種組織方式就是HTML最基礎的結構——文檔對象模型(DOM),這棵樹的每個文檔對象就叫做DOM節點。
??在渲染引擎中,DOM 有三個層面的作用:
??● 從頁面的視角來看,DOM 是生成頁面的基礎數據結構;
??● 從 JavaScript 腳本視角來看,DOM 提供給 JavaScript 腳本操作的接口,通過這套接口,JavaScript 可以對 DOM 結構進行訪問,從而改變文檔的結構、樣式和內容;
??● 從安全視角來看,DOM 是一道安全防護線,一些不安全的內容在 DOM 解析階段會被拒之門外。
??在渲染引擎內部,HTML 解析器負責將 HTML 字節流轉換為 DOM 結構,其轉化過程如下:
1. 字符流 → 詞(token)
??HTML結構會首先通過分詞器將字節流拆分為詞(token)。Token分為Tag Token 和文本 Token。下面來看一個HTML代碼是如何被拆分的:
<body><div><p>hello world</p></div> </body>??對于這段代碼,可以拆成詞:
??可以看到,Tag Token 又分 StartTag 和 EndTag,<body>、<div>、<p>就是 StartTag ,</body>、</div>、</p>就是 EndTag,分別對應圖中的藍色和紅色塊,文本 Token 對應綠色塊。
??這里會通過狀態機將字符拆分成token,所謂的狀態機就是將每個詞的特征逐個拆分成獨立的狀態,然后再將所有詞的特征字符合并起來,形成一個連通的圖結構。那為什么要使用狀態機呢?因為每讀取一個字符,都要做一次決策,這些決策都和當前的狀態有關。
??實際上,狀態機的作用就是用來做詞法分析的,將字符流分解為詞(token)。
2. 詞(token)→ DOM樹
??接下來就需要將 Token 解析為 DOM 節點,并將 DOM 節點添加到 DOM 樹中。這個過程是通過棧結構來實現的,這個棧主要用來計算節點之間的父子關系,上面步驟中生成的token會按順序壓入棧中,該過程的規則如下:
??● 如果分詞器解析出來是StartTag Token,HTML 解析器會為該 Token 創建一個 DOM 節點,然后將該節點加入到 DOM 樹中,它的父節點就是棧中相鄰的那個元素生成的節點;
??● 如果分詞器解析出來是 文本 Token,那么會生成一個文本節點,然后將該節點加入到 DOM 樹中,文本 Token 是不需要壓入到棧中,它的父節點就是當前棧頂 Token 所對應的 DOM 節點;
??● 如果分詞器解析出來的是EndTag Token,比如是 EndTag div,HTML 解析器會查看 Token 棧頂的元素是否是 StarTag div,如果是,就將 StartTag div從棧中彈出,表示該 div 元素解析完成。
??通過分詞器產生的新 Token 就這樣不停地入棧和出棧,整個解析過程就這樣一直持續下去,直到分詞器將所有字節流分詞完成。
??下面來看看這的Token棧是如何工作的,有如下HTML結構:
<html><body><div>hello juejin</div><p>hello world</p></body> </html>??開始時,HTML解析器會創建一個根為 document 的空的 DOM 結構,同時將 StartTag document 的Token壓入棧中,然后再將解析出來的第一個 StartTag html 壓入棧中,并創建一個 html 的DOM節點,添加到 document 上,這時 Token 棧和 DOM樹 如下:
??接下來body和div標簽也會和上面的過程一樣,進行入棧操作:
??隨后就會解析到 div標簽中的文本Token,渲染引擎會為該 Token 創建一個文本節點,并將該 Token 添加到 DOM 中,它的父節點就是當前 Token 棧頂元素對應的節點:
??接下來就是第一個EndTag div,這時 HTML 解析器會判斷當前棧頂元素是否是 StartTag div,如果是,則從棧頂彈出 StartTag div,如下圖所示:
??再之后的過程就和上面類似了,最終的結果如下:
CSSOM樹構建
??上面已經基本了解了DOM的構建過程,但是這個DOM結構只包含節點,并不包含任何的樣式信息。下面就來看看,瀏覽器是如何把CSS樣式應用到DOM節點上的。
??同樣,瀏覽器也是無法直接理解CSS代碼的,需要將其瀏覽器可以理解的CSSOM樹。實際上。瀏覽器在構建 DOM 樹的同時,如果樣式也加載完成了,那么 CSSOM 樹也會同步構建。CSSOM 樹和 DOM 樹類似,它主要有兩個作用:
??● 提供給 JavaScript 操作樣式的能力;
??● 為渲染樹的合成提供基礎的樣式信息。
??不過,CSSOM 樹和 DOM 樹是獨立的兩個數據結構,它們并沒有一一對應關系。DOM 樹描述的是 HTML 標簽的層級關系,CSSOM 樹描述的是選擇器之間的層級關系。可以在瀏覽器的控制臺,通過document.styleSheets命令來查看CSSOM樹:
??那CSS樣式的來源有哪些呢?
??CSS樣式的來源主要有三種:
??● 通過 link 引用的外部 CSS 樣式文件;
??● <style>標簽內的CSS樣式;
??● 元素的style屬性內嵌的CSS。
??在將CSS轉化為樹形對象之前,還需要將樣式表中的屬性值進行標準化處理,比如,當遇到以下CSS樣式:
body { font-size: 2em } p {color:blue;} div {font-weight: bold} div p {color:green;} div {color:red; }??可以看到上面CSS中有很多屬性值,比如2em、blue、red、bold等,這些數值并不能被瀏覽器直接理解。所以,需要將所有值轉化為瀏覽器渲染引擎容易理解的、標準化的計算值,這個過程就是屬性值標準化。經過標準化的過程,上面的代碼會變成這樣:
body { font-size: 32px } p {color: rgb(0, 0, 255);} div {font-weight: 700} div p {color: (0, 128, 0);} div {color: (255, 0, 0); }??可以看到,2em被解析成了32px,blue被解析成了rgb(255, 0, 0),bold被解析成700。現在樣式的屬性已被標準化了,接下來就需要計算 DOM 樹中每個節點的樣式屬性了,這就涉及到 CSS 的繼承規則和層疊規則。
(1)樣式繼承
??在 CSS 中存在樣式的繼承機制,CSS 繼承就是每個 DOM 節點都包含有父節點的樣式。比如在 HTML 上設置“font-size:20px;”,那么頁面里基本所有的標簽都可以繼承到這個屬性了。
??在CSS中,有繼承性的屬性主要有以下幾種:
?1.字體系列屬性
?● font-family:字體系列
?● font-weight:字體的粗細
?● font-size:字體的大小
?● font-style:字體的風格
?2.文本系列屬性
?● text-indent:文本縮進
text-align:文本水平對齊
?● line-height:行高
?● word-spacing:單詞之間的間距
?● letter-spacing:中文或者字母之間的間距
?● text-transform:控制文本大小寫(就是uppercase、lowercase、capitalize這三個)
?● color:文本顏色
?3.元素可見性
?● visibility:控制元素顯示隱藏
?4.列表布局屬性
?● list-style:列表風格,包括list-style-type、list-style-image等
?5.光標屬性
?● cursor:光標顯示為何種形態
(2)樣式層疊
??樣式計算過程中的第二個規則是樣式層疊。層疊是 CSS 的一個基本特征,它是一個定義了 如何合并來自多個源的屬性值的算法。它在 CSS 處于核心地位,CSS 的全稱“層疊樣式表”正是強調了這一點。這里不再多說。
??總之,樣式計算階段的目的是為了計算出 DOM 節點中每個元素的具體樣式,在計算過程 中需要遵守 CSS 的繼承和層疊兩個規則。這個階段最終輸出的內容是每個 DOM 節點的樣 式,并被保存在 ComputedStyle 的結構內。
??對于以下代碼:
<html><head><link href="./style.css"><style>.juejin {width: 100px;height: 50px;background: red;}.content {font-size: 25px;line-height: 25px;margin: 10px;}</style></head><body><div class="juejin"><div>CUGGZ</div></div><p style="color: blue" class="content"><span>hello world</span><p style="display: none;">瀏覽器</p></p></body> </html>??最終生成的CSSOM樹大致如下:
渲染樹構建
??在 DOM 樹和 CSSOM 樹都渲染完成之后,就會進入渲染樹的構建階段。渲染樹就是 DOM 樹和 CSSOM 樹的結合,會得到一個可以知道每個節點會應用什么樣式的數據結構。這個結合的過程就是遍歷整個 DOM 樹,然后在 CSSOM 樹里查詢到匹配的樣式。
???在不同瀏覽器里,構建渲染樹的過程不太一樣:
?● 在 Chrome 里會在每個節點上使用 attach() 方法,把 CSSOM 樹的節點掛在 DOM 樹上作為渲染樹。
?● 在 Firefox 里會單獨構造一個新的結構, 用來連接 DOM 樹和 CSSOM 樹的映射關系。
??那為什么要構建渲染樹呢?在上面的示例中可以看到,DOM樹可能包含一些不可見的元素,比如head標簽,使用display:none;屬性的元素等。所以在顯示頁面之前,還要額外地構建一棵只包含可見元素的渲染樹。
??下面來看看構建渲染樹的過程:
??可以看到,DOM樹中不可見的節點都沒有包含到渲染樹中。為了構建渲染樹,瀏覽器上大致做了如下工作:遍歷DOM樹中所有可見節點,并把這些節點加到布局中,而不可見的節點會被布局樹忽略掉,如 head 標簽下面的全部內容,再比如 p.p 這個元素,因為它的屬性包含 dispaly:none,所以這個元素也沒有被包含進渲染樹中。如果給元素設置了visibility: hidden屬性,那這個元素會出現在渲染樹中,因為具有這個樣式的元素是需要占位的,只不過不需要顯示出來。
??這里在查找的過程中,出于效率的考慮,會從 CSSOM 樹的葉子節點開始查找,對應在 CSS 選擇器上也就是從選擇器的最右側向左查找。所以,不建議使用標簽選擇器和通配符選擇器來定義元素樣式。
??除此之外,同一個 DOM 節點可能會匹配到多個 CSSOM 節點,而最終的效果由哪個 CSS 規則來確定,就是樣式優先級的問題了。當一個 DOM 元素受到多條樣式控制時,樣式的優先級順序如下:內聯樣式 > ID選擇器 > 類選擇器 > 標簽選擇器 > 通用選擇器 > 繼承樣式 > 瀏覽器默認樣式
??CSS常見選擇器的優先級如下:
??對于選擇器的優先級:
?● 標簽選擇器、偽元素選擇器:1;
?● 類選擇器、偽類選擇器、屬性選擇器:10;
?● id 選擇器:100;
?● 內聯樣式:1000;
??注意:
?● !important聲明的樣式的優先級最高;
?● 如果優先級相同,則最后出現的樣式生效;
?● 繼承得到的樣式的優先級最低;
頁面布局
??經過上面的步驟,就生成了一棵渲染樹,這棵樹就是展示頁面的關鍵。到現在為止,已經有了需要渲染的所有節點之間的結構關系及其樣式信息。下面就需要進行頁面的布局。
??通過計算渲染樹上每個節點的樣式,就能得出來每個元素所占空間的大小和位置。當有了所有元素的大小和位置后,就可以在瀏覽器的頁面區域里去繪制元素的邊框了。這個過程就是布局。這個過程中,瀏覽器對渲染樹進行遍歷,將元素間嵌套關系以盒模型的形式寫入文檔流:
??盒模型在布局過程中會計算出元素確切的大小和定位。計算完畢后,相應的信息被寫回渲染樹上,就形成了布局渲染樹。同時,每一個元素盒子也都攜帶著自身的樣式信息,作為后續繪制的依據。
頁面繪制
1. 構建圖層
??經過布局,每個元素的位置和大小就有了,那下面是不是就該開始繪制頁面了?答案是否定的,因為頁面上可能有很多復雜的場景,比如3D變化、頁面滾動、使用z-index進行z軸的排序等。所以,為了實現這些效果,渲染引擎還需要為特定的節點生成專用的圖層,并生成一棵對應的圖層樹。
??那什么是圖層呢?我們可以在Chrome瀏覽器的開發者工具中,選擇Layers標簽(如果沒有,可以在更多工具中查找),就可以看到頁面的分層情況,以掘金首頁為例,其分層情況如下:
??可以看到,渲染引擎給頁面分了很多圖層,這些圖層會按照一定順序疊加在一起,就形成了最終的頁面。這里,將頁面分解成多個圖層的操作就成為分層, 最后將這些圖層合并到一層的操作就成為合成, 分層和合成通常是一起使用的。Chrome 引入了分層和合成的機制就是為了提升每幀的渲染效率。
??通常情況下,并不是渲染樹上的每個節點都包含一個圖層,如果一個節點沒有對應的圖層,那這個節點就會屬于其父節點的圖層。那什么樣的節點才能讓瀏覽器引擎為其創建一個新的圖層呢?需要滿足以下其中一個條件:
(1)擁有層疊上下文屬性的元素
??我們看到的頁面通常是二維的平面,而層疊上下文能夠讓頁面具有三維的概念。這些 HTML 元素按照自身屬性的優先級分布在垂直于這個二維平面的 z 軸上。下面是盒模型的層疊規則:
??對于上圖,由上到下分別是:
?● 背景和邊框:建立當前層疊上下文元素的背景和邊框。
?● 負的z-index:當前層疊上下文中,z-index屬性值為負的元素。
?● 塊級盒:文檔流內非行內級非定位后代元素。
?● 浮動盒:非定位浮動元素。
?● 行內盒:文檔流內行內級非定位后代元素。
?● z-index:0:層疊級數為0的定位元素。
?● 正z-index:z-index屬性值為正的定位元素。
??注意: 當定位元素z-index:auto,生成盒在當前層疊上下文中的層級為 0,不會建立新的層疊上下文,除非是根元素。
(2)需要裁剪的元素
??什么是裁剪呢?假如有一個固定寬高的div盒子,而里面的文字較多超過了盒子的高度,這時就會產生裁剪,瀏覽器渲染引擎會把裁剪文字內容的一部分用于顯示在 div 區域。當出現裁剪時,瀏覽器的渲染引擎就會為文字部分單獨創建一個圖層,如果出現滾動條,那么滾動條也會被提升為單獨的圖層。
2. 繪制圖層
??在完成圖層樹的構建之后,渲染引擎會對圖層樹中的每個圖層進行繪制,下面就來看看渲染引擎是怎么實現圖層繪制的。
??渲染引擎在繪制圖層時,會把一個圖層的繪制分成很多繪制指令,然后把這些指令按照順序組成一個待繪制的列表:
??可以看到,繪制列表中的指令就是一系列的繪制操作。通常情況下,繪制一個元素需要執行多條繪制指令,因為每個元素的背景、邊框等屬性都需要單獨的指令進行繪制。所以在圖層繪制階段,輸出的內容就是繪制列表。
??在Chrome瀏覽器的開發者工具中,通過Layer標簽可以看到圖層的繪制列表和繪制過程:
??繪制列表只是用來記錄繪制順序和繪制指令的列表,而繪制操作是由渲染引擎中的合成線程來完成的。當圖層繪制列表準備好之后,主線程會把該繪制列表提交給合成線程。
??注意:合成操作是在合成線程上完成的,所以,在執行合成操作時并不會影響到主線程的執行。
??很多情況下,圖層可能很大,比如一篇長文章,需要滾動很久才能到底,但是用戶只能看到視口的內容,所以沒必要把整個圖層都繪制出來。因此,合成線程會將圖層劃分為圖塊,這些圖塊的大小通常是 256x256 或者 512x512。合成線程會優先將視口附近的圖塊生成位圖。實際生成位圖的操作是在光柵化階段來執行的,所謂的光柵化就是按照繪制列表中的指令生成圖片。
??當所有的圖塊都被光柵化之后,合成線程就會生成一個繪制圖塊的命令,瀏覽器相關進程收到這個指令之后,就會將其頁面內容繪制在內存中,最后將內存顯示在屏幕上,這樣就完成了頁面的繪制。
??至此,整個渲染流程就完成了,其過程總結如下:
?1.將HTML內容構建成DOM樹;
?2.將CSS內容構建成CSSOM樹;
?3.將DOM 樹和 CSSOM 樹合成渲染樹;
?4.根據渲染樹進行頁面元素的布局;
?5.對渲染樹進行分層操作,并生成分層樹;
?6.為每個圖層生成繪制列表,并提交到合成線程;
?7.合成線程將圖層分成不同的圖塊,并通過柵格化將圖塊轉化為位圖;
?8.合成線程給瀏覽器進程發送繪制圖塊指令;
?9.瀏覽器進程會生成頁面,并顯示在屏幕上。
擴展
1. 重排和重繪
??說完瀏覽器引擎的渲染流程,再來看兩個重要的概念:重排(Reflow)和重繪(Repaint)。
??我們知道,渲染樹是動態構建的,所以,DOM節點和CSS節點的改動都可能會造成渲染樹的重新構建。渲染樹的改動就會造成頁面的重排或者重繪。下面就來看看這兩個概念,以及它們觸發的條件和減少觸發的操作。
(1)重排
??當我們的操作引發了 DOM 樹中幾何尺寸的變化(改變元素的大小、位置、布局方式等),這時渲染樹里有改動的節點和它影響的節點都要重新計算。這個過程就叫做重排,也稱為回流。在改動發生時,要重新經歷頁面渲染的整個流程,所以開銷是很大的。
??以下操作都會導致頁面重排:
?● 頁面首次渲染;
?● 瀏覽器窗口大小發生變化;
?● 元素的內容發生變化;
?● 元素的尺寸或者位置發生變化;
?● 元素的字體大小發生變化;
?● 激活CSS偽類;
?● 查詢某些屬性或者調用某些方法;
?● 添加或者刪除可見的DOM元素。
??在觸發重排時,由于瀏覽器渲染頁面是基于流式布局的,所以當觸發回流時,會導致周圍的DOM元素重新排列,它的影響范圍有兩種:
?● 全局范圍:從根節點開始,對整個渲染樹進行重新布局;
?● 局部范圍:對渲染樹的某部分或者一個渲染對象進行重新布局。
(2)重繪
??當對 DOM 的修改導致了樣式的變化、但未影響其幾何屬性(比如修改顏色、背景色)時,瀏覽器不需重新計算元素的幾何屬性、直接為該元素繪制新的樣式(會跳過重排環節),這個過程叫做重繪。簡單來說,重繪是由對元素繪制屬性的修改引發的。
??當我們修改元素繪制屬性時,頁面布局階段不會執行,因為并沒有引起幾何位置的變換,所以就直接進入了繪制階段,然后執行之后的一系列子階段。相較于重排操作,重繪省去了布局和分層階段,所以執行效率會比重排操作要高一些。
下面這些屬性會導致回流:
?● color、background 相關屬性:background-color、background-image 等;
?● outline 相關屬性:outline-color、outline-width 、text-decoration;
?● border-radius、visibility、box-shadow。
??注意:當觸發重排時,一定會觸發重繪,但是重繪不一定會引發重排。
??相對來說,重排操作的消耗會比較大,所以在操作中盡量少的造成頁面的重排。為了減少重排,可以通過以下方式進行優化:
?● 在條件允許的情況下盡量使用 CSS3 動畫,它可以調用 GPU 執行渲染。
?● 操作DOM時,盡量在低層級的DOM節點進行操作
?● 不要使用table布局, 一個小的改動可能會使整個table進行重新布局
?● 使用CSS的表達式
?● 不要頻繁操作元素的樣式,對于靜態頁面,可以修改類名,而不是樣式。
?● 使用 absolute 或者 fixed,使元素脫離文檔流,這樣他們發生變化就不會影響其他元素
?● 避免頻繁操作DOM,可以創建一個文檔片段documentFragment,在它上面應用所有DOM操作,最后再把它添加到文檔中
?● 將元素先設置display: none,操作結束后再把它顯示出來。因為在display屬性為none的元素上進行的DOM操作不會引發回流和重繪。
?● 將DOM的多個讀操作(或者寫操作)放在一起,而不是讀寫操作穿插著寫。這得益于瀏覽器的渲染隊列機制。
??瀏覽器針對頁面的回流與重繪,進行了自身的優化——渲染隊列, 瀏覽器會將所有的回流、重繪的操作放在一個隊列中,當隊列中的操作到了一定的數量或者到了一定的時間間隔,瀏覽器就會對隊列進行批處理。這樣就會讓多次的回流、重繪變成一次回流重繪。
2. JavaScript 對 DOM 的影響
??當解析器解析HTML時,如果遇到了script標簽,判斷這是腳本,就會暫停 DOM 的解析,因為接下來的 JavaScript 腳本可能會修改當前已經生成的 DOM 結構。
??來看一段代碼:
<html><body><div>hello juejin</div><script>document.getElementsByTagName('div')[0].innerText = 'juejin yyds'</script><p>hello world</p></body> </html>??這里,當解析完div標簽后,就會解析script標簽,這時的DOM結構如下:
??這時,HTML解析器就會暫停工作,JavaScript引擎就會開始工作,并執行script標簽中的腳本內容。由于這段腳本修改了第一個div的內容,所以執行完這個腳本之后,div中的文本就變成了“juejin yyds”,當腳本執行完成之后,HTML解析器就會恢復解析過程,繼續解析后面的內容,直至生成最終的DOM。
??上面我們說的JavaScript腳本是通過script標簽直接嵌入到HTML中的。當在頁面中引入JavaScript腳本時,情況就會變得復雜。比如:
<html><body><div>hello juejin</div><script type="text/javascript" src='./index.js'></script><p>hello world</p></body> </html>??其實這里的執行流程和上面時一樣的,當遇到script標簽時,HTML解析器都會暫停解析并去執行腳本文件。不過這里執行 JavaScript 腳本時,需要先下載腳本。腳本的下載過程會阻塞 DOM 的解析,而通常下載又是非常耗時的,會受到網絡環境、JavaScript 腳本文件大小等因素的影響。
??經過上面的分析可知,JavaScript 線程會阻塞 DOM 的解析,我們可以通過CDN、壓縮腳本等方式來加速 JavaScript 腳本的加載。如果腳本文件中沒有操作DOM的相關代碼,就可以將JavaScript腳本設置為異步加載,可以給script標簽添加 async 或 defer 屬性來實現腳本的異步加載。兩者的使用方式如下:
<script async type="text/javascript" src='./index.js'></script> <script defer type="text/javascript" src='./index.js'></script>??下圖可以直觀的看出異步加載和直接加載的區別:
??其中藍色代表腳本下載,紅色代表腳本執行,綠色代表HTML解析,灰色表示HTML解析暫停。
當初始HTML文檔已完全加載和解析時,將觸發DOMContentLoaded事件,而不需要等待樣式表,圖像和子框架頁面加載。該事件可以用來檢測HTML頁面是否完全加載完畢。
??defer 和 async屬性都是去異步加載外部的JS腳本文件,它們都不會阻塞頁面的解析,其區別如下:
?● 執行順序: 多個帶async屬性的標簽,不能保證加載的順序;多個帶defer屬性的標簽,按照加載順序執行;
?● 腳本是否并行執行: async屬性,表示后續文檔的加載和執行與js腳本的加載和執行是并行進行的,即異步執行;defer屬性,加載后續文檔的過程和js腳本的加載(此時僅加載不執行)是并行進行的(異步),JavaScript 腳本需要等到文檔所有元素解析完成之后才執行,DOMContentLoaded事件觸發執行之前。
??再來看另外一種情況:
<html><head><style src='./style.css'></style></head><body><div>hello juejin</div><script>const ele = document.getElementsByTagName('div')[0];ele.innerText = 'juejin yyds'; // 操作DOMele.style.color = 'skyblue'; // 操作CSSOM</script><p>hello world</p></body> </html>??上面的代碼中,第9行是操作DOM的,而第10行是操作CSSOM的,所以在執行 JavaScript 腳本之前,還需要先解析 JavaScript 語句之上所有的 CSS 樣式。所以如果代碼里引用了外部的 CSS 文件,那么在執行 JavaScript 之前,還需要 等待外部的 CSS 文件下載完成,并解析生成 CSSOM 對象之后,才能執行 JavaScript 腳本。而 JavaScript 引擎在解析 JavaScript 之前,是不知道 JavaScript 是否操縱了 CSSOM 的,所以渲染引擎在遇到 JavaScript 腳本時,不管該腳本是否操縱了 CSSOM,都會執行 CSS 文件下載,解析操作,再執行 JavaScript 腳本。
??所以,JavaScript 會阻塞 DOM 生成,而樣式文件又會阻塞 JavaScript 的執行,我們在開發時需要格外注意這一點。
??最后再來看一種情況,示例代碼如下:
<html><head><style src='./style.css'></style></head><body><div>hello juejin</div><script type="text/javascript" src='./index.js'></script><p>hello world</p></body> </html>??這段HTML代碼中包含了CSS外部引用和JavaScript外部文件,在接收到 HTML 數據之后的預解析過程中,HTML 預解析器識別出來了有 CSS 文件和 JavaScript 文件需要下載,就會同時發起兩個文件的下載請求。
總結
以上是生活随笔為你收集整理的详解浏览器渲染原理及流程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mindmanager的R6025 -
- 下一篇: RCAR会议:我的RTFA算法里面的ge