React 渲染機制 — — Virtual DOM 與 Diffing 演算法

QQAI
SWF Lab
Published in
6 min readFeb 25, 2023

--

前言

React 身為時下最流行的開發框架之一,擁有的優勢有元件化思考、JSX 語法以及有效率的 Virtual DOM 更新作為其渲染機制。
這篇文章將會介紹 React 是如何運用 Diffing 演算法與 Virtual DOM 的概念來節省資源。

感謝 趙昱森UN SWEET 幫我 Review 這篇文章。(>人<;)

Outline

  • DOM
  • Virtual DOM
  • Diffing 演算法
  • Conclusion

DOM (Document Object Model)

對網頁開發稍微了解的人應該都對 DOM 這個詞有印象。
當瀏覽器在讀取我們撰寫的 HTML 程式碼時,每發現一個 HTML 元素,就會建構一個身為 javascript 物件的 Node,再進而建出一棵樹,我們稱之為 DOM Tree。

而當我們修改了這棵樹當中的任一個 Node,就需要渲染機制去幫我們做 DOM 的更新,重新畫上一個新的元素。

<!DOCTYPE html>
<html lang="en">
<head>
<title>Example1</title>
</head>
<body>
<h1>Hello world</h1>
</body>
</html>
由上述程式碼所建立出的 DOM Tree。

而目前常見的畫面渲染策略有兩種:

  • 第一種就是「透過人為手動去判斷需要更新的地方有哪些,再根據更新內容進行精確的修改」
    例如我們想要透過按下 Button 替換 h1 當中的文字,就利用 js 程式碼去抓取 html 裡的元素,並對其進行更新,但當 DOM Tree 變得龐大,就非常考驗開發者的設計能力。
  • 第二種則是「只要有資料變更,就將整個畫面重新渲染」,好處是開發者不需要絞盡腦汁設計渲染策略,壞處則是明顯的效能浪費。

Virtual DOM

大致瞭解了上述的兩種渲染策略以後,來介紹一下 React 是如何克服缺點的。

React 的解決思路是利用 Virtual DOM 作為橋樑,因為重新繪製整個 DOM 太耗費效能了,那就先畫在虛擬的 DOM Tree 上,再比較差異作出必要的更新吧!

React 根據這個思想去設計了 Virtual DOM,他顧名思義並不是一個真正由瀏覽器所產生並存在其中的 DOM,而是存在 memory 當中的一個 javascript 物件。

在平時,React 會將 DOM 裡跟渲染有關的資料結構複製一份存在 memory 當中,每當有事件或是資料產生變動(例如 useState() 函式裏的 state 值有了變動),再與新的程式碼所產生的 Virtual DOM Tree 進行 Diffing 演算法的比較,找出不同之處,再根據其對真正的 DOM 進行更新渲染。

比較之後,DOM 最後只會更新黃色的新增節點。

即便操作 Virtual DOM 也會需要吃一部份的效能,但跟改動整個 DOM 結構相比,節省了非常多的資源。

Diffing 演算法

了解了 Virtual DOM 的概念以後,接著介紹剛剛提出的 Diffing 演算法。

當 DOM Tree 非常龐大的時候,要比對兩棵樹之間的差異,並且用最少的操作去替換舊的元素,即便是用目前已知由哥本哈根技術大學提出的最有效率的比較樹演算法,也需要時間複雜度 O(n³) 的時間,n 為 Tree 當中 element 的數量。

也就是說,假設今天有 100 個 DOM element,就需要 100 * 100 * 100 也就是一百萬次的比較,更何況 DOM 結構更加龐大的情形,容易造成效能巨大的負擔。

於是 React 官方就想出了另一種 heuristic (啟發式)的解法,這個解法應用了 BFS(Breadth-First-Search)的理念,原因是當 React 找到一個與原 Tree 不同的節點時,在該節點之下的 Tree 都必須被更新,因此 DFS(Depth-First-Search)在此並不是最好的思路,廣泛地找尋不同的節點才是更優先的作法。

這個解法就是 Diffing 演算法,藉由使每一個子 element 都能有一個獨一無二的 key value,並比較這些 key value 來找出差異,這能為 React 的渲染機制達到 O(n) 的時間複雜度,而解法要成立必須實現以下兩點假設:

  • 兩個不同的 element 會產生出不同的 Tree。
  • 開發者能夠透過 key value 指出哪些 child element 在不同的渲染下依然能夠維持不變。

目前經由 React 的開發與測試,這兩個條件幾乎在所有實務場景都能成立。

這裡也提供 React 官方給予的即時範例網頁,有一些互動式的例子能幫助大家更好的了解。

但由於 Diffing 演算法是啟發式的演算法,也就是說在以下條件未能成立時,效能將會有所影響:

  • Diffing 演算法不會檢查相異 component 類型的 subtree。如果發現所撰寫的程式網頁在兩種不同類型的 component 中做出切換,但渲染出同樣的內容,建議將其改成同一類型。
    註:React 官方表示,目前還未發現在改成同一種類型後會發生問題的情況。
例如當你有兩個不同的 Component,卻會輸出同樣的 element subtree 時,React 並不會特別因為 Component 不同而做出差異檢查,於是建議直接將其改為同一個 Component 類型命名即可。
  • Key value 應該具有穩定、可預測、以及 array 內「唯一」的幾項特質。不穩定的 key value,例如 random 隨機生成的 value,將會導致許多 component instance 和 DOM 節點被不必要地重新建立,這容易導致效能低下,以及遺失 child component 中的 state。

Conclusion

綜上所述,React 的渲染機制就是透過 Virtual DOM 與 Diffing 演算法,達到 Reconciliation,也就是比對差異、對帳的效果。

對 React 的渲染機制有更深入的了解以後,在做網頁的更新規劃時,也可以依據渲染機制的特性去做設計與思考,更好地優化大家網頁的效能。

如果對這篇文章的內容有疑惑之處也歡迎聯絡我跟我討論!學海無涯!(>人<;)

References :

--

--