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,備注:大前端;進前端進階群;


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM