1. 程式人生 > >JS進階 - 瀏覽器工作原理

JS進階 - 瀏覽器工作原理

一、瀏覽器的結構

瀏覽器的主要元件為:

  • 使用者介面 - 包括位址列、前進/後退按鈕、書籤選單等。除了瀏覽器主視窗(顯示頁面),其他部分都屬於使用者介面。
  • 瀏覽器引擎 - 在使用者介面和渲染引擎之間傳送指令。
  • 渲染引擎 - 顯示(渲染)請求的內容。如果請求的內容是 HTML,它就負責解析 HTML 和 CSS 內容,並將解析後的內容顯示在螢幕上。
  • 網路 - 用於網路呼叫,比如 HTTP 請求。其介面與平臺無關,併為所有平臺提供底層實現。
  • 使用者介面後端 - 用於繪製基本的視窗小部件,比如組合框和視窗。公開了與平臺無關的通用介面,在底層使用作業系統的使用者介面方法。
  • JavaScript 直譯器。用於解析和執行 JavaScript 程式碼。
  • 資料儲存。這是持久層。瀏覽器需要在硬碟上儲存各種資料,例如 Cookie。新的 HTML 規範 (HTML5) 定義了“網路資料庫”,這是一個完整(但是輕便)的瀏覽器內資料庫。

二、渲染引擎

渲染引擎負責渲染——即渲染HTML/XML文件或者圖片(通過外掛可以渲染PDF等等)。渲染引擎有

  • Chrome/Safari - Webkit
  • Firefox - Gecko
  • Edge - EdgeHTML(不在本文討論範圍)

(一)渲染主流程

瀏覽器從網路層獲取請求的文件內容,然後開始渲染流程:

  • 解析並開始構建 content tree(element --> DOM nodes),同時解析樣式資料(外部CSS和style元素);
  • 兩者結合構建 render tree(渲染樹包含帶有視覺屬性(如顏色和尺寸)的矩形們)
  • 在渲染樹建立後進入 Layout 階段,給渲染樹的每個節點設定在螢幕上的位置資訊
  • Paint 階段,通過 UI backend 繪製 render tree 到螢幕。

注意,渲染過程是漸進式的。瀏覽器會盡早展示文件內容,即不會在所有HTML文件解析完成後才會去構建render tree,而是部分內容被解析和展示,並繼續解析和展示剩下的。

對chrome而言,渲染的具體流程是

對firefox而言,

(二)處理指令碼和樣式表的順序

  1. script 是同步的

    web模型一直是同步的,即網頁作者希望引擎遇到<script>標籤時可以立即解析並執行——停止解析HTML,執行指令碼(如果是外部指令碼,先下載)。可以用defer屬性指定指令碼是非同步的——不會停止文件解析,在文件解析完成後執行。

  2. Speculative parsing(預解析)

    當執行指令碼時,其它執行緒會解析剩下的文件,找出裡面的外部資源(script/style/img)來提前載入(可以並行載入)。這種解析只是去查詢需要載入的外部資源,不會修改content tree。

    所以我們可以看到多個外部資源並行下載。

  3. 樣式

    樣式表有不同的模型。理論上,樣式表不會更改 DOM tree,似乎沒有必要等待樣式表並停止文件解析。但有個問題,如果在文件解析階段,指令碼訪問樣式資訊怎麼辦?Firefox會在指令碼載入和解析階段禁止所有的指令碼;對於 WebKit 而言,僅當指令碼嘗試訪問的樣式屬性可能受尚未載入的樣式表影響時,它才會禁止該指令碼。

這就是為什麼推薦樣式放在<head>裡而指令碼放在<body>底部。

(三)Render tree construction

構建 DOM tree的同時,瀏覽器還會構建另一個樹:渲染樹(render tree)。這是由視覺化元素按照其顯示順序而組成的樹,也是文件的視覺化表示。它的作用是保證按照正確的順序來繪製內容。

渲染樹的每個節點(renderer)代表一個矩形區域——對應DOM元素的CSS Box。

renderer 和 DOM元素對應,但非一一對應。比如display:none的元素沒有對應的renderer;比如select對應3個renderer(display area/drop down list box /button)。另外,根據css spec,一個inline元素只能包含一個block元素或者多個inline元素,如果不符規則,就會建立anonymous block renderer。

有些 renderers 與對應的 DOM 節點,在各自樹中的位置不同。比如浮動定位和絕對定位的元素,它們在normal flow之外,放置在樹的其它地方,並對映到真正的renderer,而放在原位的是placeholder renderer。

漸進式處理

WebKit 使用一個標記來表示是否所有的頂級樣式表(包括 @imports)均已載入完畢。如果在attaching(DOM+CSSOM --> Render tree)過程中樣式尚未完全載入,則使用佔位符,並在文件中進行標註,等樣式表載入完畢後再重新計算。

(四)Layout

renderer在建立完成並新增到render tree時,並不包含 位置和大小 資訊。計算這些值的過程稱為佈局或重排(Layout/Reflow)。

HTML 採用基於流的佈局模型,這意味著大多數情況下只要一次遍歷就能計算出幾何資訊。處於流中靠後位置元素通常不會影響靠前位置元素的幾何特徵,因此佈局可以按從左至右、從上至下的順序遍歷文件。

Dirty 位系統

為避免對所有細小更改都進行整體佈局,瀏覽器採用了一種“dirty 位”系統。如果renderer有更改,或者其自身及其children被標註為“dirty”——則需要進行佈局。

有兩種標記:“dirty”和“children are dirty”。“children are dirty”表示renderer自身沒有變化,但它的children需要佈局。

全域性佈局和增量佈局

全域性佈局是指觸發了整個render tree的佈局,觸發原因可能包括:

  • 影響所有renderers的全域性樣式更改,例如字型大小更改。
  • 螢幕大小調整。

佈局可以採用增量方式,也就是隻對 dirty 的 renderer 進行佈局(這樣可能存在需要進行額外佈局的弊端)。

當renderer為 dirty 時,觸發增量佈局(非同步)。例如,當來自網路的額外內容新增到 DOM 樹之後,新的renderer附加到了render tree中。

非同步佈局和同步佈局

  • 增量佈局是非同步執行的。

    請求樣式資訊(如“offsetHeight”)的指令碼可觸發同步增量佈局。

  • 全域性佈局往往是同步執行的。

  • 有時,當初始佈局完成之後,如果一些屬性(如滾動位置)發生變化,佈局就會作為回撥而觸發。

優化

  • 如果layout由 resize 或者 renderer 的位置變化觸發,那麼尺寸就無需再計算,直接從快取獲取;
  • 有些情況如果只是子樹變化(比如text更新),那麼layout無需從root開始。

佈局處理

佈局過程通常如下:

  • 父renderer確定自己的寬度。

  • 父renderer依次處理子renderer,並且:

    • 放置子renderer(設定 x,y 座標)。
    • 如果有必要,呼叫子renderer的佈局(如果子renderer是 dirty 的,或者這是全域性佈局,或出於其他某些原因),這會計運算元renderer的高度。
  • 父renderer根據子renderer的累加高度以及邊距和補白的高度來設定自身高度,此值也可供父renderer的父renderer使用。

  • 將其 dirty 位設定為 false。

寬度計算

renderer寬度是根據容器塊(container block)的寬度、renderer樣式中的“width”屬性以及邊距和邊框計算得出的。

換行

如果renderer在佈局過程中需要換行,會立即停止佈局,並告知其父renderer需要換行。父renderer會建立額外的renderer,並對其呼叫佈局。

(五)Painting

在繪製階段,會遍歷render tree,並呼叫renderer的“paint”方法,將renderer的內容顯示在螢幕上。繪製工作是使用使用者介面基礎元件(UI infrastructure component)完成的。

全域性繪製和增量繪製

和佈局一樣,繪製也分為全域性(繪製整個render tree)和增量兩種。在增量繪製中,部分renderer發生了更改,但是不會影響整個樹。更改後的renderer將其在螢幕上對應的矩形區域設為無效,這導致 OS 將其視為一塊“dirty 區域”,並生成“paint”事件。OS 會很巧妙地將多個區域合併成一個。

繪製順序

CSS2 defines the order of the painting process. This is actually the order in which the elements are stacked in the stacking contexts. This order affects painting since the stacks are painted from back to front.

block renderer的堆疊順序是:

  1. 背景顏色
  2. 背景圖片
  3. 邊框
  4. children
  5. 輪廓(outline)

動態變化

在發生變化時,瀏覽器會盡可能做出最小的響應。比如元素的顏色改變後,只會對該元素進行重繪。元素的位置改變後,只會對該元素及其子元素(可能還有同級元素)進行佈局和重繪。新增 DOM 節點後,會對該節點進行佈局和重繪。

一些重大變化(例如增大“html”元素的字型)會導致快取無效,使得整個render tree都會進行重新佈局和繪製。

結合整個render tree構建和lauout,paint階段,可以去思考怎麼減少relayout/repaint。

渲染引擎的執行緒(The rendering engine's threads)

渲染引擎是單執行緒的。幾乎所有操作(除了網路操作)都是在單執行緒中進行的。在 Firefox 和 Safari 中,該執行緒就是瀏覽器的主執行緒。而在 Chrome 瀏覽器中,該執行緒是tab程序的主執行緒。

網路操作可由多個執行緒並行執行。並行連線數是有限的(通常為 2~6 個)。

Event loop

The browser main thread is an event loop. It's an infinite loop that keeps the process alive. It waits for events (like layout and paint events) and processes them.

這裡可配合 #21 閱讀,結合上面一小段,可展開討論下。

在瀏覽器的具體實現裡,瀏覽器核心(渲染程序)是多執行緒的。其中最重要的執行緒有:

  • GUI執行緒,即本章所講的渲染引擎執行緒,負責解析HTML/CSS,構建DOM tree和 render tree,佈局和繪製等。

    頁面第一次展示,或者需要重繪(repaint)或由於某種操作引發迴流(reflow)時,該執行緒執行。

  • JS執行緒,即JS引擎執行緒,負責解析JavaScript指令碼,執行程式碼。JS引擎一直等待著任務佇列中任務的到來,然後執行。

    一個Tab頁(渲染程序)中無論什麼時候都只有一個JS執行緒在執行——JS是單執行緒的。

  • 其它執行緒。

GUI執行緒和JS執行緒是互斥的(因為JavaScript可操縱DOM)。這就是為什麼JS長時間執行會導致瀏覽器失去響應。

 


 

加微信:boan910227,備註:大前端;進前端進階群;