日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 前端技术 > HTML >内容正文

HTML

图解浏览器渲染原理及流程

發(fā)布時(shí)間:2024/1/1 HTML 32 豆豆
生活随笔 收集整理的這篇文章主要介紹了 图解浏览器渲染原理及流程 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

本文約?8500?字,預(yù)計(jì)閱讀需要 30?分鐘。

今天來(lái)分享一下瀏覽器的渲染原理及流程。

前言

先來(lái)看看 Chrome 瀏覽器的多進(jìn)程架構(gòu):

通常,我們打包出來(lái)的 HTML、CSS、JavaScript 等文件,經(jīng)過(guò)瀏覽器運(yùn)行之后就會(huì)顯示出頁(yè)面,這個(gè)過(guò)程就是瀏覽器的渲染進(jìn)程來(lái)操作實(shí)現(xiàn)的,渲染進(jìn)程的主要任務(wù)就是將靜態(tài)資源轉(zhuǎn)化為可視化界面:

對(duì)于中間的瀏覽器,它就是一個(gè)黑盒,下面就來(lái)看看這個(gè)黑盒是如何將靜態(tài)資源轉(zhuǎn)化為前端界面的。由于渲染機(jī)制比較復(fù)雜,所以渲染模塊在執(zhí)行過(guò)程中會(huì)被劃分為很多子階段,輸入的靜態(tài)資源經(jīng)過(guò)這些子階段,最后輸出頁(yè)面。我們將一個(gè)處理流程稱為渲染流水線,其大致流程如下圖所示:

這里主要包含五個(gè)過(guò)程:

  • DOM樹(shù)構(gòu)建:渲染引擎使用HTML解析器(調(diào)用XML解析器)解析HTML文檔,將各個(gè)HTML元素逐個(gè)轉(zhuǎn)化成DOM節(jié)點(diǎn),從而生成DOM樹(shù);

  • CSSOM樹(shù)構(gòu)建:CSS解析器解析CSS,并將其轉(zhuǎn)化為CSS對(duì)象,將這些CSS對(duì)象組裝起來(lái),構(gòu)建CSSOM樹(shù);

  • 渲染樹(shù)構(gòu)建:DOM 樹(shù)和 CSSOM 樹(shù)都構(gòu)建完成以后,瀏覽器會(huì)根據(jù)這兩棵樹(shù)構(gòu)建出一棵渲染樹(shù);

  • 頁(yè)面布局:渲染樹(shù)構(gòu)建完畢之后,元素的位置關(guān)系以及需要應(yīng)用的樣式就確定了,這時(shí)瀏覽器會(huì)計(jì)算出所有元素的大小和絕對(duì)位置;

  • 頁(yè)面繪制:頁(yè)面布局完成之后,瀏覽器會(huì)將根據(jù)處理出來(lái)的結(jié)果,把每一個(gè)頁(yè)面圖層轉(zhuǎn)換為像素,并對(duì)所有的媒體文件進(jìn)行解碼。

對(duì)于這五個(gè)流程,每一階段都有對(duì)應(yīng)的產(chǎn)物:DOM樹(shù)、CSSOM樹(shù)、渲染樹(shù)、盒模型、界面。

下圖為渲染引擎工作流程中各個(gè)步驟所對(duì)應(yīng)的模塊:

從圖中可以看出,渲染引擎主要包含的模塊有:

  • HTML 解析器:解析HTML文檔,主要作用是將HTML文檔轉(zhuǎn)換成DOM樹(shù);

  • CSS 解析器:將DOM中的各個(gè)元素對(duì)象進(jìn)行計(jì)算,獲取樣式信息,用于渲染樹(shù)的構(gòu)建;

  • JavaScript 解釋器:使用JavaScript可以修改網(wǎng)頁(yè)的內(nèi)容、CSS規(guī)則等。JavaScript解釋器能夠解釋JavaScript代碼,并通過(guò)DOM接口和CSSOM接口來(lái)修改網(wǎng)頁(yè)內(nèi)容、樣式規(guī)則,從而改變渲染結(jié)果;

  • 頁(yè)面布局:DOM創(chuàng)建之后,渲染引擎將其中的元素對(duì)象與樣式規(guī)則進(jìn)行結(jié)合,可以得到渲染樹(shù)。布局則是針對(duì)渲染樹(shù),計(jì)算其各個(gè)元素的大小、位置等布局信息。

  • 頁(yè)面繪制:使用圖形庫(kù)將布局計(jì)算后的渲染樹(shù)繪制成可視化的圖像結(jié)果。

DOM樹(shù)構(gòu)建

在說(shuō)構(gòu)建DOM樹(shù)之前,我們需要知道,為什么要構(gòu)建DOM樹(shù)呢? 這是因?yàn)?#xff0c;瀏覽器無(wú)法直接理解和使用 HTML,所以需要將HTML轉(zhuǎn)化為瀏覽器能夠理解的結(jié)構(gòu)——DOM樹(shù)。

了解過(guò)數(shù)據(jù)結(jié)構(gòu)的小伙伴對(duì)于樹(shù)結(jié)構(gòu)應(yīng)該不陌生,樹(shù)是由結(jié)點(diǎn)或頂點(diǎn)和邊組成的且不存在著任何環(huán)的一種數(shù)據(jù)結(jié)構(gòu)。一棵非空的樹(shù)包括一個(gè)根結(jié)點(diǎn),還有多個(gè)附加結(jié)點(diǎn),所有結(jié)點(diǎn)構(gòu)成一個(gè)多級(jí)分層結(jié)構(gòu)。下面通過(guò)一張圖來(lái)看看什么是樹(shù)結(jié)構(gòu):

對(duì)于上面的三個(gè)結(jié)構(gòu),前兩個(gè)都是樹(shù),他們都只有唯一的根節(jié)點(diǎn),而且不存在環(huán)結(jié)構(gòu)。而第三個(gè)存在環(huán),所以就不是一個(gè)樹(shù)結(jié)構(gòu)。

在頁(yè)面中,每個(gè)HTML標(biāo)簽都會(huì)被瀏覽器解析成文檔對(duì)象。HTML本質(zhì)上就是一個(gè)嵌套結(jié)構(gòu),在解析時(shí)會(huì)把每個(gè)文檔對(duì)象用一個(gè)樹(shù)形結(jié)構(gòu)組織起來(lái),所有的文檔對(duì)象都會(huì)掛在document上,這種組織方式就是HTML最基礎(chǔ)的結(jié)構(gòu)——文檔對(duì)象模型(DOM),這棵樹(shù)的每個(gè)文檔對(duì)象就叫做DOM節(jié)點(diǎn)。

在渲染引擎中,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 解析階段會(huì)被拒之門(mén)外。

在渲染引擎內(nèi)部,HTML 解析器負(fù)責(zé)將 HTML 字節(jié)流轉(zhuǎn)換為 DOM 結(jié)構(gòu),其轉(zhuǎn)化過(guò)程如下:

1. 字符流 → 詞(token)

HTML結(jié)構(gòu)會(huì)首先通過(guò)分詞器將字節(jié)流拆分為詞(token)。Token分為T(mén)ag Token 和文本 Token。下面來(lái)看一個(gè)HTML代碼是如何被拆分的:

<body><div><p>hello?world</p></div> </body>

對(duì)于這段代碼,可以拆成詞:

可以看到,Tag Token 又分 StartTag 和 EndTag,<body>、<div>、<p>就是 StartTag ,</body>、</div>、</p>就是 EndTag,分別對(duì)應(yīng)圖中的藍(lán)色和紅色塊,文本 Token 對(duì)應(yīng)綠色塊。

這里會(huì)通過(guò)狀態(tài)機(jī)將字符拆分成token,所謂的狀態(tài)機(jī)就是將每個(gè)詞的特征逐個(gè)拆分成獨(dú)立的狀態(tài),然后再將所有詞的特征字符合并起來(lái),形成一個(gè)連通的圖結(jié)構(gòu)。那為什么要使用狀態(tài)機(jī)呢?因?yàn)槊孔x取一個(gè)字符,都要做一次決策,這些決策都和當(dāng)前的狀態(tài)有關(guān)。

實(shí)際上,狀態(tài)機(jī)的作用就是用來(lái)做詞法分析的,將字符流分解為詞(token)。

2. 詞(token)→ DOM樹(shù)

接下來(lái)就需要將 Token 解析為 DOM 節(jié)點(diǎn),并將 DOM 節(jié)點(diǎn)添加到 DOM 樹(shù)中。這個(gè)過(guò)程是通過(guò)棧結(jié)構(gòu)來(lái)實(shí)現(xiàn)的,這個(gè)棧主要用來(lái)計(jì)算節(jié)點(diǎn)之間的父子關(guān)系,上面步驟中生成的token會(huì)按順序壓入棧中,該過(guò)程的規(guī)則如下:

  • 如果分詞器解析出來(lái)是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 Token,比如是 EndTag div,HTML 解析器會(huì)查看 Token 棧頂?shù)脑厥欠袷?StarTag div,如果是,就將 StartTag div從棧中彈出,表示該 div 元素解析完成。

通過(guò)分詞器產(chǎn)生的新 Token 就這樣不停地入棧和出棧,整個(gè)解析過(guò)程就這樣一直持續(xù)下去,直到分詞器將所有字節(jié)流分詞完成。

下面來(lái)看看這的Token棧是如何工作的,有如下HTML結(jié)構(gòu):

<html><body><div>hello?juejin</div><p>hello?world</p></body> </html>

開(kāi)始時(shí),HTML解析器會(huì)創(chuàng)建一個(gè)根為 document 的空的 DOM 結(jié)構(gòu),同時(shí)將 StartTag document 的Token壓入棧中,然后再將解析出來(lái)的第一個(gè) StartTag html 壓入棧中,并創(chuàng)建一個(gè) html 的DOM節(jié)點(diǎn),添加到 document 上,這時(shí) Token 棧和 DOM樹(shù) 如下:

接下來(lái)body和div標(biāo)簽也會(huì)和上面的過(guò)程一樣,進(jìn)行入棧操作:

隨后就會(huì)解析到 div標(biāo)簽中的文本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)就是第一個(gè)EndTag div,這時(shí) HTML 解析器會(huì)判斷當(dāng)前棧頂元素是否是 StartTag div,如果是,則從棧頂彈出 StartTag div,如下圖所示:

再之后的過(guò)程就和上面類似了,最終的結(jié)果如下:

CSSOM樹(shù)構(gòu)建

上面已經(jīng)基本了解了DOM的構(gòu)建過(guò)程,但是這個(gè)DOM結(jié)構(gòu)只包含節(jié)點(diǎn),并不包含任何的樣式信息。下面就來(lái)看看,瀏覽器是如何把CSS樣式應(yīng)用到DOM節(jié)點(diǎn)上的。

同樣,瀏覽器也是無(wú)法直接理解CSS代碼的,需要將其瀏覽器可以理解的CSSOM樹(shù)。實(shí)際上。瀏覽器在構(gòu)建 DOM 樹(shù)的同時(shí),如果樣式也加載完成了,那么 CSSOM 樹(shù)也會(huì)同步構(gòu)建。CSSOM 樹(shù)和 DOM 樹(shù)類似,它主要有兩個(gè)作用:

  • 提供給 JavaScript 操作樣式的能力;

  • 為渲染樹(shù)的合成提供基礎(chǔ)的樣式信息。

不過(guò),CSSOM 樹(shù)和 DOM 樹(shù)是獨(dú)立的兩個(gè)數(shù)據(jù)結(jié)構(gòu),它們并沒(méi)有一一對(duì)應(yīng)關(guān)系。DOM 樹(shù)描述的是 HTML 標(biāo)簽的層級(jí)關(guān)系,CSSOM 樹(shù)描述的是選擇器之間的層級(jí)關(guān)系。可以在瀏覽器的控制臺(tái),通過(guò)document.styleSheets命令來(lái)查看CSSOM樹(shù):

那CSS樣式的來(lái)源有哪些呢?

CSS樣式的來(lái)源主要有三種:

  • 通過(guò) link 引用的外部 CSS 樣式文件;

  • <style>標(biāo)簽內(nèi)的CSS樣式;

  • 元素的style屬性內(nèi)嵌的CSS。

在將CSS轉(zhuǎn)化為樹(shù)形對(duì)象之前,還需要將樣式表中的屬性值進(jìn)行標(biāo)準(zhǔn)化處理,比如,當(dāng)遇到以下CSS樣式:

body?{?font-size:?2em?} p?{color:blue;} div?{font-weight:?bold} div?p?{color:green;} div?{color:red;?}

可以看到上面CSS中有很多屬性值,比如2em、blue、red、bold等,這些數(shù)值并不能被瀏覽器直接理解。所以,需要將所有值轉(zhuǎn)化為瀏覽器渲染引擎容易理解的、標(biāo)準(zhǔn)化的計(jì)算值,這個(gè)過(guò)程就是屬性值標(biāo)準(zhǔn)化。經(jīng)過(guò)標(biāo)準(zhǔn)化的過(guò)程,上面的代碼會(huì)變成這樣:

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。現(xiàn)在樣式的屬性已被標(biāo)準(zhǔn)化了,接下來(lái)就需要計(jì)算 DOM 樹(shù)中每個(gè)節(jié)點(diǎn)的樣式屬性了,這就涉及到 CSS 的繼承規(guī)則和層疊規(guī)則。

(1)樣式繼承

在 CSS 中存在樣式的繼承機(jī)制,CSS 繼承就是每個(gè) DOM 節(jié)點(diǎn)都包含有父節(jié)點(diǎn)的樣式。比如在 HTML 上設(shè)置“font-size:20px;”,那么頁(yè)面里基本所有的標(biāo)簽都可以繼承到這個(gè)屬性了。

在CSS中,有繼承性的屬性主要有以下幾種:

  • 字體系列屬性

    • font-family:字體系列

    • font-weight:字體的粗細(xì)

    • font-size:字體的大小

    • font-style:字體的風(fēng)格

  • 文本系列屬性

    • text-indent:文本縮進(jìn)

    • text-align:文本水平對(duì)齊

    • line-height:行高

    • word-spacing:單詞之間的間距

    • letter-spacing:中文或者字母之間的間距

    • text-transform:控制文本大小寫(xiě)(就是uppercase、lowercase、capitalize這三個(gè))

    • color:文本顏色

  • 元素可見(jiàn)性

    • visibility:控制元素顯示隱藏

  • 列表布局屬性

    • list-style:列表風(fēng)格,包括list-style-type、list-style-image等

  • 光標(biāo)屬性

    • cursor:光標(biāo)顯示為何種形態(tài)

    (2)樣式層疊

    樣式計(jì)算過(guò)程中的第二個(gè)規(guī)則是樣式層疊。層疊是 CSS 的一個(gè)基本特征,它是一個(gè)定義了 如何合并來(lái)自多個(gè)源的屬性值的算法。它在 CSS 處于核心地位,CSS 的全稱“層疊樣式表”正是強(qiáng)調(diào)了這一點(diǎn)。這里不再多說(shuō)。

    總之,樣式計(jì)算階段的目的是為了計(jì)算出 DOM 節(jié)點(diǎn)中每個(gè)元素的具體樣式,在計(jì)算過(guò)程 中需要遵守 CSS 的繼承和層疊兩個(gè)規(guī)則。這個(gè)階段最終輸出的內(nèi)容是每個(gè) DOM 節(jié)點(diǎn)的樣 式,并被保存在 ComputedStyle 的結(jié)構(gòu)內(nèi)。

    對(duì)于以下代碼:

    <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樹(shù)大致如下:

    渲染樹(shù)構(gòu)建

    在 DOM 樹(shù)和 CSSOM 樹(shù)都渲染完成之后,就會(huì)進(jìn)入渲染樹(shù)的構(gòu)建階段。渲染樹(shù)就是 DOM 樹(shù)和 CSSOM 樹(shù)的結(jié)合,會(huì)得到一個(gè)可以知道每個(gè)節(jié)點(diǎn)會(huì)應(yīng)用什么樣式的數(shù)據(jù)結(jié)構(gòu)。這個(gè)結(jié)合的過(guò)程就是遍歷整個(gè) DOM 樹(shù),然后在 CSSOM 樹(shù)里查詢到匹配的樣式。

    在不同瀏覽器里,構(gòu)建渲染樹(shù)的過(guò)程不太一樣:

    • 在 Chrome 里會(huì)在每個(gè)節(jié)點(diǎn)上使用 attach() 方法,把 CSSOM 樹(shù)的節(jié)點(diǎn)掛在 DOM 樹(shù)上作為渲染樹(shù)。

    • 在 Firefox 里會(huì)單獨(dú)構(gòu)造一個(gè)新的結(jié)構(gòu), 用來(lái)連接 DOM 樹(shù)和 CSSOM 樹(shù)的映射關(guān)系。

    那為什么要構(gòu)建渲染樹(shù)呢?在上面的示例中可以看到,DOM樹(shù)可能包含一些不可見(jiàn)的元素,比如head標(biāo)簽,使用display:none;屬性的元素等。所以在顯示頁(yè)面之前,還要額外地構(gòu)建一棵只包含可見(jiàn)元素的渲染樹(shù)

    下面來(lái)看看構(gòu)建渲染樹(shù)的過(guò)程:

    可以看到,DOM樹(shù)中不可見(jiàn)的節(jié)點(diǎn)都沒(méi)有包含到渲染樹(shù)中。為了構(gòu)建渲染樹(shù),瀏覽器上大致做了如下工作:遍歷DOM樹(shù)中所有可見(jiàn)節(jié)點(diǎn),并把這些節(jié)點(diǎn)加到布局中,而不可見(jiàn)的節(jié)點(diǎn)會(huì)被布局樹(shù)忽略掉,如 head 標(biāo)簽下面的全部?jī)?nèi)容,再比如 p.p 這個(gè)元素,因?yàn)樗膶傩园?dispaly:none,所以這個(gè)元素也沒(méi)有被包含進(jìn)渲染樹(shù)中。如果給元素設(shè)置了visibility: hidden屬性,那這個(gè)元素會(huì)出現(xiàn)在渲染樹(shù)中,因?yàn)榫哂羞@個(gè)樣式的元素是需要占位的,只不過(guò)不需要顯示出來(lái)。

    這里在查找的過(guò)程中,出于效率的考慮,會(huì)從 CSSOM 樹(shù)的葉子節(jié)點(diǎn)開(kāi)始查找,對(duì)應(yīng)在 CSS 選擇器上也就是從選擇器的最右側(cè)向左查找。所以,不建議使用標(biāo)簽選擇器和通配符選擇器來(lái)定義元素樣式。

    除此之外,同一個(gè) DOM 節(jié)點(diǎn)可能會(huì)匹配到多個(gè) CSSOM 節(jié)點(diǎn),而最終的效果由哪個(gè) CSS 規(guī)則來(lái)確定,就是樣式優(yōu)先級(jí)的問(wèn)題了。當(dāng)一個(gè) DOM 元素受到多條樣式控制時(shí),樣式的優(yōu)先級(jí)順序如下:內(nèi)聯(lián)樣式 > ID選擇器 > 類選擇器 > 標(biāo)簽選擇器 > 通用選擇器 > 繼承樣式 > 瀏覽器默認(rèn)樣式

    CSS常見(jiàn)選擇器的優(yōu)先級(jí)如下:

    選擇器格式優(yōu)先級(jí)權(quán)重
    id選擇器#id100
    類選擇器.classname10
    屬性選擇器a[ref=“eee”]10
    偽類選擇器li:last-child10
    標(biāo)簽選擇器div1
    偽元素選擇器li:after1
    相鄰兄弟選擇器h1+p
    子選擇器ul>li
    后代選擇器li a
    通配符選擇器*

    對(duì)于選擇器的優(yōu)先級(jí)

    • 標(biāo)簽選擇器、偽元素選擇器:1;

    • 類選擇器、偽類選擇器、屬性選擇器:10;

    • id 選擇器:100;

    • 內(nèi)聯(lián)樣式:1000;

    注意:

    • !important聲明的樣式的優(yōu)先級(jí)最高;

    • 如果優(yōu)先級(jí)相同,則最后出現(xiàn)的樣式生效;

    • 繼承得到的樣式的優(yōu)先級(jí)最低;

    頁(yè)面布局

    經(jīng)過(guò)上面的步驟,就生成了一棵渲染樹(shù),這棵樹(shù)就是展示頁(yè)面的關(guān)鍵。到現(xiàn)在為止,已經(jīng)有了需要渲染的所有節(jié)點(diǎn)之間的結(jié)構(gòu)關(guān)系及其樣式信息。下面就需要進(jìn)行頁(yè)面的布局。

    通過(guò)計(jì)算渲染樹(shù)上每個(gè)節(jié)點(diǎn)的樣式,就能得出來(lái)每個(gè)元素所占空間的大小和位置。當(dāng)有了所有元素的大小和位置后,就可以在瀏覽器的頁(yè)面區(qū)域里去繪制元素的邊框了。這個(gè)過(guò)程就是布局。這個(gè)過(guò)程中,瀏覽器對(duì)渲染樹(shù)進(jìn)行遍歷,將元素間嵌套關(guān)系以盒模型的形式寫(xiě)入文檔流:

    盒模型在布局過(guò)程中會(huì)計(jì)算出元素確切的大小和定位。計(jì)算完畢后,相應(yīng)的信息被寫(xiě)回渲染樹(shù)上,就形成了布局渲染樹(shù)。同時(shí),每一個(gè)元素盒子也都攜帶著自身的樣式信息,作為后續(xù)繪制的依據(jù)。

    頁(yè)面繪制

    1. 構(gòu)建圖層

    經(jīng)過(guò)布局,每個(gè)元素的位置和大小就有了,那下面是不是就該開(kāi)始繪制頁(yè)面了?答案是否定的,因?yàn)轫?yè)面上可能有很多復(fù)雜的場(chǎng)景,比如3D變化、頁(yè)面滾動(dòng)、使用z-index進(jìn)行z軸的排序等。所以,為了實(shí)現(xiàn)這些效果,渲染引擎還需要為特定的節(jié)點(diǎn)生成專用的圖層,并生成一棵對(duì)應(yīng)的圖層樹(shù)。

    那什么是圖層呢?我們可以在Chrome瀏覽器的開(kāi)發(fā)者工具中,選擇Layers標(biāo)簽(如果沒(méi)有,可以在更多工具中查找),就可以看到頁(yè)面的分層情況,以掘金首頁(yè)為例,其分層情況如下:

    可以看到,渲染引擎給頁(yè)面分了很多圖層,這些圖層會(huì)按照一定順序疊加在一起,就形成了最終的頁(yè)面。這里,將頁(yè)面分解成多個(gè)圖層的操作就成為分層, 最后將這些圖層合并到一層的操作就成為合成, 分層和合成通常是一起使用的。Chrome 引入了分層和合成的機(jī)制就是為了提升每幀的渲染效率。

    通常情況下,并不是渲染樹(shù)上的每個(gè)節(jié)點(diǎn)都包含一個(gè)圖層,如果一個(gè)節(jié)點(diǎn)沒(méi)有對(duì)應(yīng)的圖層,那這個(gè)節(jié)點(diǎn)就會(huì)屬于其父節(jié)點(diǎn)的圖層。那什么樣的節(jié)點(diǎn)才能讓瀏覽器引擎為其創(chuàng)建一個(gè)新的圖層呢?需要滿足以下其中一個(gè)條件:

    (1)擁有層疊上下文屬性的元素

    我們看到的頁(yè)面通常是二維的平面,而層疊上下文能夠讓頁(yè)面具有三維的概念。這些 HTML 元素按照自身屬性的優(yōu)先級(jí)分布在垂直于這個(gè)二維平面的 z 軸上。下面是盒模型的層疊規(guī)則:

    對(duì)于上圖,由上到下分別是:

    • 背景和邊框:建立當(dāng)前層疊上下文元素的背景和邊框。

    • 負(fù)的z-index:當(dāng)前層疊上下文中,z-index屬性值為負(fù)的元素。

    • 塊級(jí)盒:文檔流內(nèi)非行內(nèi)級(jí)非定位后代元素。

    • 浮動(dòng)盒:非定位浮動(dòng)元素。

    • 行內(nèi)盒:文檔流內(nèi)行內(nèi)級(jí)非定位后代元素。

    • z-index:0:層疊級(jí)數(shù)為0的定位元素。

    • 正z-index:z-index屬性值為正的定位元素。

    注意: 當(dāng)定位元素z-index:auto,生成盒在當(dāng)前層疊上下文中的層級(jí)為 0,不會(huì)建立新的層疊上下文,除非是根元素。

    (2)需要裁剪的元素

    什么是裁剪呢?假如有一個(gè)固定寬高的div盒子,而里面的文字較多超過(guò)了盒子的高度,這時(shí)就會(huì)產(chǎn)生裁剪,瀏覽器渲染引擎會(huì)把裁剪文字內(nèi)容的一部分用于顯示在 div 區(qū)域。當(dāng)出現(xiàn)裁剪時(shí),瀏覽器的渲染引擎就會(huì)為文字部分單獨(dú)創(chuàng)建一個(gè)圖層,如果出現(xiàn)滾動(dòng)條,那么滾動(dòng)條也會(huì)被提升為單獨(dú)的圖層。

    2. 繪制圖層

    在完成圖層樹(shù)的構(gòu)建之后,渲染引擎會(huì)對(duì)圖層樹(shù)中的每個(gè)圖層進(jìn)行繪制,下面就來(lái)看看渲染引擎是怎么實(shí)現(xiàn)圖層繪制的。

    渲染引擎在繪制圖層時(shí),會(huì)把一個(gè)圖層的繪制分成很多繪制指令,然后把這些指令按照順序組成一個(gè)待繪制的列表:

    可以看到,繪制列表中的指令就是一系列的繪制操作。通常情況下,繪制一個(gè)元素需要執(zhí)行多條繪制指令,因?yàn)槊總€(gè)元素的背景、邊框等屬性都需要單獨(dú)的指令進(jìn)行繪制。所以在圖層繪制階段,輸出的內(nèi)容就是繪制列表。

    在Chrome瀏覽器的開(kāi)發(fā)者工具中,通過(guò)Layer標(biāo)簽可以看到圖層的繪制列表和繪制過(guò)程:

    繪制列表只是用來(lái)記錄繪制順序和繪制指令的列表,而繪制操作是由渲染引擎中的合成線程來(lái)完成的。當(dāng)圖層繪制列表準(zhǔn)備好之后,主線程會(huì)把該繪制列表提交給合成線程。

    注意:合成操作是在合成線程上完成的,所以,在執(zhí)行合成操作時(shí)并不會(huì)影響到主線程的執(zhí)行。

    很多情況下,圖層可能很大,比如一篇長(zhǎng)文章,需要滾動(dòng)很久才能到底,但是用戶只能看到視口的內(nèi)容,所以沒(méi)必要把整個(gè)圖層都繪制出來(lái)。因此,合成線程會(huì)將圖層劃分為圖塊,這些圖塊的大小通常是 256x256 或者 512x512。合成線程會(huì)優(yōu)先將視口附近的圖塊生成位圖。實(shí)際生成位圖的操作是在光柵化階段來(lái)執(zhí)行的,所謂的光柵化就是按照繪制列表中的指令生成圖片。

    當(dāng)所有的圖塊都被光柵化之后,合成線程就會(huì)生成一個(gè)繪制圖塊的命令,瀏覽器相關(guān)進(jìn)程收到這個(gè)指令之后,就會(huì)將其頁(yè)面內(nèi)容繪制在內(nèi)存中,最后將內(nèi)存顯示在屏幕上,這樣就完成了頁(yè)面的繪制。

    至此,整個(gè)渲染流程就完成了,其過(guò)程總結(jié)如下:

  • 將HTML內(nèi)容構(gòu)建成DOM樹(shù);

  • 將CSS內(nèi)容構(gòu)建成CSSOM樹(shù);

  • 將DOM 樹(shù)和 CSSOM 樹(shù)合成渲染樹(shù);

  • 根據(jù)渲染樹(shù)進(jìn)行頁(yè)面元素的布局;

  • 對(duì)渲染樹(shù)進(jìn)行分層操作,并生成分層樹(shù);

  • 為每個(gè)圖層生成繪制列表,并提交到合成線程;

  • 合成線程將圖層分成不同的圖塊,并通過(guò)柵格化將圖塊轉(zhuǎn)化為位圖;

  • 合成線程給瀏覽器進(jìn)程發(fā)送繪制圖塊指令;

  • 瀏覽器進(jìn)程會(huì)生成頁(yè)面,并顯示在屏幕上。

  • 擴(kuò)展

    1. 重排和重繪

    說(shuō)完瀏覽器引擎的渲染流程,再來(lái)看兩個(gè)重要的概念:重排(Reflow)和重繪(Repaint)。

    我們知道,渲染樹(shù)是動(dòng)態(tài)構(gòu)建的,所以,DOM節(jié)點(diǎn)和CSS節(jié)點(diǎn)的改動(dòng)都可能會(huì)造成渲染樹(shù)的重新構(gòu)建。渲染樹(shù)的改動(dòng)就會(huì)造成頁(yè)面的重排或者重繪。下面就來(lái)看看這兩個(gè)概念,以及它們觸發(fā)的條件和減少觸發(fā)的操作。

    (1)重排

    當(dāng)我們的操作引發(fā)了 DOM 樹(shù)中幾何尺寸的變化(改變?cè)氐拇笮 ⑽恢谩⒉季址绞降?#xff09;,這時(shí)渲染樹(shù)里有改動(dòng)的節(jié)點(diǎn)和它影響的節(jié)點(diǎn)都要重新計(jì)算。這個(gè)過(guò)程就叫做重排,也稱為回流。在改動(dòng)發(fā)生時(shí),要重新經(jīng)歷頁(yè)面渲染的整個(gè)流程,所以開(kāi)銷是很大的。

    以下操作都會(huì)導(dǎo)致頁(yè)面重排:

    • 頁(yè)面首次渲染;

    • 瀏覽器窗口大小發(fā)生變化;

    • 元素的內(nèi)容發(fā)生變化;

    • 元素的尺寸或者位置發(fā)生變化;

    • 元素的字體大小發(fā)生變化;

    • 激活CSS偽類;

    • 查詢某些屬性或者調(diào)用某些方法;

    • 添加或者刪除可見(jiàn)的DOM元素。

    在觸發(fā)重排時(shí),由于瀏覽器渲染頁(yè)面是基于流式布局的,所以當(dāng)觸發(fā)回流時(shí),會(huì)導(dǎo)致周?chē)腄OM元素重新排列,它的影響范圍有兩種:

    • 全局范圍:從根節(jié)點(diǎn)開(kāi)始,對(duì)整個(gè)渲染樹(shù)進(jìn)行重新布局;

    • 局部范圍:對(duì)渲染樹(shù)的某部分或者一個(gè)渲染對(duì)象進(jìn)行重新布局。

    (2)重繪

    當(dāng)對(duì) DOM 的修改導(dǎo)致了樣式的變化、但未影響其幾何屬性(比如修改顏色、背景色)時(shí),瀏覽器不需重新計(jì)算元素的幾何屬性、直接為該元素繪制新的樣式(會(huì)跳過(guò)重排環(huán)節(jié)),這個(gè)過(guò)程叫做重繪。簡(jiǎn)單來(lái)說(shuō),重繪是由對(duì)元素繪制屬性的修改引發(fā)的。

    當(dāng)我們修改元素繪制屬性時(shí),頁(yè)面布局階段不會(huì)執(zhí)行,因?yàn)椴](méi)有引起幾何位置的變換,所以就直接進(jìn)入了繪制階段,然后執(zhí)行之后的一系列子階段。相較于重排操作,重繪省去了布局和分層階段,所以執(zhí)行效率會(huì)比重排操作要高一些。

    下面這些屬性會(huì)導(dǎo)致回流:

    • color、background 相關(guān)屬性:background-color、background-image 等;

    • outline 相關(guān)屬性:outline-color、outline-width 、text-decoration;

    • border-radius、visibility、box-shadow。

    注意:當(dāng)觸發(fā)重排時(shí),一定會(huì)觸發(fā)重繪,但是重繪不一定會(huì)引發(fā)重排。

    相對(duì)來(lái)說(shuō),重排操作的消耗會(huì)比較大,所以在操作中盡量少的造成頁(yè)面的重排。為了減少重排,可以通過(guò)以下方式進(jìn)行優(yōu)化:

    • 在條件允許的情況下盡量使用 CSS3 動(dòng)畫(huà),它可以調(diào)用 GPU 執(zhí)行渲染。

    • 操作DOM時(shí),盡量在低層級(jí)的DOM節(jié)點(diǎn)進(jìn)行操作

    • 不要使用table布局, 一個(gè)小的改動(dòng)可能會(huì)使整個(gè)table進(jìn)行重新布局

    • 使用CSS的表達(dá)式

    • 不要頻繁操作元素的樣式,對(duì)于靜態(tài)頁(yè)面,可以修改類名,而不是樣式。

    • 使用 absolute 或者 fixed,使元素脫離文檔流,這樣他們發(fā)生變化就不會(huì)影響其他元素

    • 避免頻繁操作DOM,可以創(chuàng)建一個(gè)文檔片段documentFragment,在它上面應(yīng)用所有DOM操作,最后再把它添加到文檔中

    • 將元素先設(shè)置display: none,操作結(jié)束后再把它顯示出來(lái)。因?yàn)樵赿isplay屬性為none的元素上進(jìn)行的DOM操作不會(huì)引發(fā)回流和重繪。

    • 將DOM的多個(gè)讀操作(或者寫(xiě)操作)放在一起,而不是讀寫(xiě)操作穿插著寫(xiě)。這得益于瀏覽器的渲染隊(duì)列機(jī)制

    瀏覽器針對(duì)頁(yè)面的回流與重繪,進(jìn)行了自身的優(yōu)化——渲染隊(duì)列, 瀏覽器會(huì)將所有的回流、重繪的操作放在一個(gè)隊(duì)列中,當(dāng)隊(duì)列中的操作到了一定的數(shù)量或者到了一定的時(shí)間間隔,瀏覽器就會(huì)對(duì)隊(duì)列進(jìn)行批處理。這樣就會(huì)讓多次的回流、重繪變成一次回流重繪。

    2. JavaScript 對(duì) DOM 的影響

    當(dāng)解析器解析HTML時(shí),如果遇到了script標(biāo)簽,判斷這是腳本,就會(huì)暫停 DOM 的解析,因?yàn)榻酉聛?lái)的 JavaScript 腳本可能會(huì)修改當(dāng)前已經(jīng)生成的 DOM 結(jié)構(gòu)。

    來(lái)看一段代碼:

    <html><body><div>hello?juejin</div><script>document.getElementsByTagName('div')[0].innerText?=?'juejin?yyds'</script><p>hello?world</p></body> </html>

    這里,當(dāng)解析完div標(biāo)簽后,就會(huì)解析script標(biāo)簽,這時(shí)的DOM結(jié)構(gòu)如下:

    這時(shí),HTML解析器就會(huì)暫停工作,JavaScript引擎就會(huì)開(kāi)始工作,并執(zhí)行script標(biāo)簽中的腳本內(nèi)容。由于這段腳本修改了第一個(gè)div的內(nèi)容,所以執(zhí)行完這個(gè)腳本之后,div中的文本就變成了“juejin yyds”,當(dāng)腳本執(zhí)行完成之后,HTML解析器就會(huì)恢復(fù)解析過(guò)程,繼續(xù)解析后面的內(nèi)容,直至生成最終的DOM。

    上面我們說(shuō)的JavaScript腳本是通過(guò)script標(biāo)簽直接嵌入到HTML中的。當(dāng)在頁(yè)面中引入JavaScript腳本時(shí),情況就會(huì)變得復(fù)雜。比如:

    <html><body><div>hello?juejin</div><script?type="text/javascript"?src='./index.js'></script><p>hello?world</p></body> </html>

    其實(shí)這里的執(zhí)行流程和上面時(shí)一樣的,當(dāng)遇到script標(biāo)簽時(shí),HTML解析器都會(huì)暫停解析并去執(zhí)行腳本文件。不過(guò)這里執(zhí)行 JavaScript 腳本時(shí),需要先下載腳本。腳本的下載過(guò)程會(huì)阻塞 DOM 的解析,而通常下載又是非常耗時(shí)的,會(huì)受到網(wǎng)絡(luò)環(huán)境、JavaScript 腳本文件大小等因素的影響。

    經(jīng)過(guò)上面的分析可知,JavaScript 線程會(huì)阻塞 DOM 的解析,我們可以通過(guò)CDN、壓縮腳本等方式來(lái)加速 JavaScript 腳本的加載。如果腳本文件中沒(méi)有操作DOM的相關(guān)代碼,就可以將JavaScript腳本設(shè)置為異步加載,可以給script標(biāo)簽添加 async 或 defer 屬性來(lái)實(shí)現(xiàn)腳本的異步加載。兩者的使用方式如下:

    <script?async?type="text/javascript"?src='./index.js'></script> <script?defer?type="text/javascript"?src='./index.js'></script>

    下圖可以直觀的看出異步加載和直接加載的區(qū)別:

    其中藍(lán)色代表腳本下載,紅色代表腳本執(zhí)行,綠色代表HTML解析,灰色表示HTML解析暫停。

    當(dāng)初始HTML文檔已完全加載和解析時(shí),將觸發(fā)DOMContentLoaded事件,而不需要等待樣式表,圖像和子框架頁(yè)面加載。該事件可以用來(lái)檢測(cè)HTML頁(yè)面是否完全加載完畢。

    defer 和 async屬性都是去異步加載外部的JS腳本文件,它們都不會(huì)阻塞頁(yè)面的解析,其區(qū)別如下:

    • 執(zhí)行順序: 多個(gè)帶async屬性的標(biāo)簽,不能保證加載的順序;多個(gè)帶defer屬性的標(biāo)簽,按照加載順序執(zhí)行;

    • 腳本是否并行執(zhí)行: async屬性,表示后續(xù)文檔的加載和執(zhí)行與js腳本的加載和執(zhí)行是并行進(jìn)行的,即異步執(zhí)行;defer屬性,加載后續(xù)文檔的過(guò)程和js腳本的加載(此時(shí)僅加載不執(zhí)行)是并行進(jìn)行的(異步),JavaScript 腳本需要等到文檔所有元素解析完成之后才執(zhí)行,DOMContentLoaded事件觸發(fā)執(zhí)行之前。

    再來(lái)看另外一種情況:

    <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的,所以在執(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 腳本。

    所以,JavaScript 會(huì)阻塞 DOM 生成,而樣式文件又會(huì)阻塞 JavaScript 的執(zhí)行,我們?cè)陂_(kāi)發(fā)時(shí)需要格外注意這一點(diǎn)。

    最后再來(lái)看一種情況,示例代碼如下:

    <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 數(shù)據(jù)之后的預(yù)解析過(guò)程中,HTML 預(yù)解析器識(shí)別出來(lái)了有 CSS 文件和 JavaScript 文件需要下載,就會(huì)同時(shí)發(fā)起兩個(gè)文件的下載請(qǐng)求。

    最后

    我是小前端,歡迎大家圍觀我的朋友圈,搞搞技術(shù),吹吹牛逼。我的微信:kujian89,秒添加,邀你進(jìn)入 500人前端群。

    推薦閱讀

    從瀏覽器多進(jìn)程到JS單線程,JS運(yùn)行機(jī)制最全面的一次梳理

    肝完《瀏覽器基本原理與實(shí)踐》的精華分享

    淺談瀏覽器多進(jìn)程與JS線程

    關(guān)注公眾號(hào):前端開(kāi)發(fā)博客

  • 回復(fù)「小抄」,領(lǐng)取Vue、JavaScript 和 WebComponent 小抄 PDF

  • 回復(fù)「Vue腦圖」獲取 Vue 相關(guān)腦圖

  • 回復(fù)「思維圖」獲取 JavaScript 相關(guān)思維圖

  • 回復(fù)「簡(jiǎn)歷」獲取簡(jiǎn)歷制作建議

  • 回復(fù)「簡(jiǎn)歷模板」獲取精選的簡(jiǎn)歷模板

  • 回復(fù)「加群」進(jìn)入500人前端精英群

  • 回復(fù)「電子書(shū)」下載我整理的大量前端資源,含面試、Vue實(shí)戰(zhàn)項(xiàng)目、CSS和JavaScript電子書(shū)等。

  • 回復(fù)「知識(shí)點(diǎn)」下載高清JavaScript知識(shí)點(diǎn)圖譜

  • ?👍🏻?點(diǎn)贊?+?在看?支持小編

    總結(jié)

    以上是生活随笔為你收集整理的图解浏览器渲染原理及流程的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

    如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。