[React] DOM, Virtual DOM 與 React element

Monica
13 min readFeb 23, 2024

--

《React 思維進化》 筆記系列
1. [React] DOM, Virtual DOM 與 React element
2. [React] 了解 JSX 與其語法、畫面渲染技巧
3. [React] 單向資料流介紹以及 component 初探
4. [React] 認識狀態管理機制 state 與畫面更新機制 reconciliation
5. [React] 在子 component 觸發父 component 資料更新、深入理解 batch update 與 updater function
6. [React] 了解 immutable state 與 immutable update 方法
7. [React] 認識 component 的生命週期、了解每次 render 都有自己版本的資料
8. [React] React 中的副作用處理、初探 useEffect
9. [React] 對 hooks 的 dependencies 誠實,以維護資料流的連動
10. [React] React 18 effect 函式執行兩次的原因及 useEffect 常見情境
11. [React] 認識 useCallback、useMemo,了解 hooks 運作原理

前言

近期開始讀《React 思維進化》這本書,也參與了讀書會和其他夥伴們一起討論書中內容,雖然讀書會都會有該次導讀人分享重點,但畢竟不是完全由自己輸出(?,覺得要理解透徹還是要自己消化並輸出比較有效,因此想依據書中章節順序,撰寫文章說明相關概念,文章內容會以書中敘述為主,輔以相關資料,因此部分內容可能是書中沒有、而是我另外查找的資訊,範例程式碼我會盡量改寫、不完全和書中相同,示意圖有些是我按照自己的理解重繪的,也不一定和書中圖完全相同,想了解完整書籍內容歡迎大家去買書看哦XD。此系列內容若有任何錯誤歡迎大家回覆告訴我~

此篇文章主要敘述的是書中 2–1 ~ 2–3 章節。

DOM 與 Virtual DOM

DOM 是什麼?

在我之前的文章 [WebAPIs] 淺談 JavaScript DOM Manipulation (1): 選取與樣式操作 有稍微提過,但並沒有詳細解釋;DOM 全名是 Document Object Model,是一種樹狀資料結構,表示瀏覽器中的畫面元素,需注意的是,DOM 本身不是 JavaScript 語言的一部分,而是瀏覽器提供的表示方法(MDN)。

開發者可直接操縱 DOM 來改變畫面元素,當我們操作 DOM 改變畫面時,因 DOM 與瀏覽器渲染引擎有緊密連動,渲染引擎將自動執行一系列的更新來重新繪製畫面,那一系列的更新與繪製畫面到底是什麼流程呢?瀏覽器渲染畫面的流程大致如下:

  1. 瀏覽器讀取 HTML 後產生 DOM Tree
  2. 讀取 HTML 中的 CSS 生成 CSSOM Tree,其資訊來源包含行內、嵌入或是外部引用的 CSS 樣式
  3. DOM Tree 與 CSSOM Tree 共同生成 Render Tree
  4. Layout 階段:依據 Render Tree 為各個節點排版,生成 Layout Tree,計算每個元素在螢幕上的尺寸與位置(此過程又被稱作 reflow 或是 browser reflow)
  5. Paint 階段:因各個元素可能有重疊關係,瀏覽器會建構 layer (圖層) 來管理各元素的位置、外觀、層次等關係;此階段會分開繪製多個 layer
  6. Compositing 階段:此階段已擁有多個預期繪製到畫面上的 layer,Compositing Thread 會將 Layer 切分成更小的單位 tile (柵格),由 raster threads 將 tiles rasterize (柵格化)後,再送到 GPU 去顯示到畫面上

(相關文章:【Web】瀏覽器如何繪製網頁?探討 DOM、CSSOM 與渲染(翻譯)Day08 X 瀏覽器架構演進史 & 渲染機制

從上面描述可看到,操作 DOM 會讓瀏覽器重新渲染畫面並經過一系列好多的步驟,如果每次我們要更改畫面元素時都要這樣操作,會消耗瀏覽器的很多資源,進而產生效能問題,因此,如何「以最小範圍的 DOM 操作來完成所需的畫面變動」是前端效能優化關鍵之一。

Virtual DOM 是什麼?

Virtual DOM 透過樹狀的資料結構描述實際 DOM 元素預計要長的樣子,有點像是一個夢想藍圖(?,描述開發者期待的實際 DOM 樣貌,例如我期待有一個 h1 的 DOM 元素,且 id 為 'my-title',內容文字是 'This is my title';但這只是我的期待,並沒有任何實際 DOM 產生。

Virtual DOM 是一種程式設計概念,其目的是為了有效管理 UI 結構,但如何設計、如何實作、如何與實際 DOM 對應,並沒有一個固定標準,不同前端框架有不同實作方式。

Virtual DOM 與實際 DOM 是單向同步化關係,簡單示意圖如下,可看到是我們先透過 Virtual DOM 定義我們期待的 DOM 元素樣貌,之後再轉換為實際的 DOM,是 Virtual DOM => DOM 單向的同步化關係。

所以 Virtual DOM 這樣的概念到底有什麼優點?或是說他想解決什麼問題呢? 透過 Virtual DOM,我們可採用一種「在新舊畫面的彩排間找差異」的畫面更新策略,當畫面需要更新時,我們根據以下流程來更新 DOM 元素:

  1. 完整的重新產生新畫面的新 Virtual DOM 結構,作為新畫面的彩排結果(預期新畫面的樣貌)
  2. 比較新版 Virtual DOM 與舊版 Virtual DOM,找出具體差異之處,此差異處就是這次畫面更新真正需要操作 DOM 的地方
  3. 根據新舊 Virtual DOM 差異之處,以最小範圍的 DOM 操作來完成本次畫面更新

這個流程有什麼好處? 此方法可將 DOM 操作範圍最小化,不會發生「我想改列表中的其中一個項目的標題文字,結果我一次操作整個列表 DOM,讓列表元素全部重新渲染」的狀況,如果想改標題文字,在比較新舊 Virtual DOM 差異後,就只會操作標題的 DOM 元素做更改,列表的其他元素不被影響,以減少效能成本。

雖然產生、比較 Virtual DOM 也需效能成本,但 Virtual DOM 沒有和瀏覽器渲染引擎直接綁定,對 Virtual DOM 的操作不會觸發一系列的渲染流程,而只是單純在操作 JavaScript 物件,因此大範圍畫面變動時還是有效能優勢。

BTW,所以有用 Virtual DOM 就會比直接操作 DOM 好嗎? 其實不一定,如果開發者能精確知道要變動的 DOM 元素、做最小的變動,且知道如何撰寫效能好的 DOM 操作,在這前提下,理論上直接操作 DOM 會比使用 Virtual DOM 的效能好;然而,並不是所有開發者都熟悉如何精確操作最小範圍的 DOM,Virtual DOM 可協助開發者達到 DOM 操作範圍最小化,大部分情況都能讓效能在一定水準之上,避免更大或潛在損失,同時也可讓開發者將注意力移到其他開發細節上,提升開發體驗。關於 Virtual DOM 的討論可參考 Virtual DOM is pure overhead

React Element

React element 是 React 基於 Virtual DOM 概念所實現的虛擬畫面結構元素,是描述並組成畫面的最小單位,以 JavaScript 物件型態呈現。

如何建立 React element?

透過 createElement 建立一個 React element:

import React from ‘react’;
const reactElement = React.createElement(
"h1", //元素類型
{ id: "my-titile" }, //屬性
"This is my title" //子元素
);

產生出來的 React element 是一個 JavaScript 物件:

{
type: 'h1',
props: { id: 'my-titile', children: 'This is my title'},
key: null,
ref: null,
$$typeof: Symbol('react.element')
}

將這個 React element 交由 React 轉換處理後,就可自動產生實際 DOM 畫面。

React element 在建立後是不可被修改的

Virtual DOM 在 React 的實作就是 React element,而 React element 是用來描述某個時間點版本的畫面結構(就像畫面結構的歷史紀錄)。可以想像成你送出購物訂單後,會收到一個購物車快照,紀錄你購物當下的商品金額、贈品、折價等資訊,這些資訊是記錄當下的狀態,即使之後商品更改價格,也不會影響過去的這些資訊。

因此,有畫面更新需求時.應建立全新的 React element 來表達新畫面結構,而非修改舊的 React element。

Render React element

在瀏覽器環境,我們透過 react-dom 將 React element 轉換並繪製成實際的 DOM,流程如下:

  1. 準備一個輸出實際 DOM 結果的目標容器
    在 HTML 建立一個空的 div 作為容器,React 產生的實際 DOM 元素就會注入到這,給 id 這樣我們之後可以透過 querySelector 的方式選取到這個 div
<body>
<div id='root'>
<!-- 之後 React 轉換輸出的實際 DOM 就會注入在這 -->
</div>
</body>

2. 建立 root 並指定目標容器
index.js 取得這容器,呼叫 ReactDOM.createRoot 來事先用這容器元素產生一個「root」,也就是 React 產生並管理 DOM 輸出結果的「畫面渲染管轄入口」(root 是一個 React 和實際 DOM 連接的控制器,不是畫面的一部分,也不是 React element 哦)

import React from "react";
import { createRoot } from "react-dom/client"; //用於瀏覽器 DOM 環境,能將 React element 轉換成實際的 DOM element

const rootElement = document.getElementById("root"); //取得HTML中事先定義好的容器元素
const root = createRoot(rootElement); //用這個容器元素來建立一個 React APP 畫面渲染管轄入口(root)

3. 準備一個用來描述畫面的 React element


const reactElement = React.createElement(
"h1",
{ id: "my-titile" },
"This is my title"
);

4. 以建立好的 root 來將 React element 繪製成實際的 DOM element

root.render(reactElement); // 將 React element 轉換渲染成實際的 DOM element

補充

  • 如果 root 容器裡有其他非 React 產生的 DOM 元素怎麼辦?
    容器內所有非 React 產生的 DOM 元素,會被 React 轉換出來的 DOM 元素全部覆蓋掉,因為 React 要確保 root 容器內的 DOM 與 React element 結構一致。
  • root 只能有一個嗎?
    React 支援一個前端應用有多個 root 存在,但如果應用是完全的 SPA,建議一個 root 即可;如果要和不同前端解決方案整合,可用多個 root。

React 只會去操作那些真正需要被更新的 DOM element

更新畫面時,React 會產生新的 React element,自動進行新舊 React element 的比較並尋找差異之處,只操作這些差異處所對應的 DOM element,在沒有更新需求的元素重用舊有的 React element,以達到效能優化。

瀏覽器環境以外的 React 畫面繪製

Virtual DOM 概念將畫面管理的流程分離成兩個獨立的階段,分別為「定義及管理畫面結構描述」與「將畫面結構的描述繪製成實際畫面成品」,對應 React 的實作就是 Reconciler 與 Renderer。

Reconciler 的任務是「定義及管理畫面結構描述」,在瀏覽器的實作中,它會定義並產生新 React element 來描述預期 DOM 結構,有畫面更新需求時,會比較新舊 React element 的差異,並交給 Renderer 處理。

Renderer 的任務是「將畫面結構的描述繪製成實際畫面成品」,在瀏覽器的實作中,瀏覽器的 Renderer 就是 react-dom,會將 React element 繪製成實際 DOM,並在有畫面更新需求時,將 Reconciler 已經比較出來的新舊差異處,同步化到實際 DOM 更新與操作。

補充:React 16 重寫 Fiber 後,除了 Reconciler 和 Renderer,還多了「scheduler」,負責將 Reconciler 的工作碎片化並進行調度管理,以達效能優化。相關文章:React 開發者一定要知道的底層機制 — React Fiber Reconciler

階段拆分有什麼好處?

  • reconciler 各環境都能通用,只要該環境可以跑 JavaScript,開發者就可在其他環境用熟悉的 API 與 React 互動,達到「learn once, write anywhere」效果。
  • renderer 可以任意被替換,達到平台解耦,只要有支援該目標環境的 renderer 配合,React 也可用於管理並產生瀏覽器 DOM 以外的 UI 或畫面。
    如:react-dom是瀏覽器的 renderer,react-native 則是 Android/iOS APP 的 renderer…,更多 renderer 可見 awesome-react-renderer

--

--