---
# System prepended metadata

title: 'React 是如何運作的？How React work?（Render, Virtual DOM, Reconciliation, Commit）'

---

# React 是如何運作的？How React work?（Render, Virtual DOM, Reconciliation, Commit）




</br>

![](https://i.imgur.com/D8zBRxw.png)


</br>

:::danger
心理建設：
- 新手上路，請抱持開放心胸，不要完全相信我。
- 如有講錯，歡迎提出，鞭小力點。
:::

</br>

### 前情提要

</br>

如阿華師所云，我們是先「畫好設計圖」，再由 React 幫我們「畫到網頁上」。也曾聽聞 React 有「virtual DOM」這個東西，來代替我們直接操作網頁實際的 DOM。

而今天就是要再稍微深入了解下， React 是怎麼做到這些事的？


</br>

### 為什麼不直接操作 DOM？

當網站的互動變更複雜，也就是「資料」與「畫面」的關係變複雜，這個時候，直接去操作 DOM 就會變成一件效率不好的事情。

</br>

當我們直接操作 DOM，當瀏覽器在更新畫面的時候會觸發以下這兩個更新機制：

- repaint（重繪）
    - 畫面元素更換樣式時候就觸發（background-color、color…）
- reflow（回流）
    - 更改畫面的佈局(結構排列)就會觸發（更改視窗大小，操作css屬性：position、width、height…）

</br>


> 網頁許多問題都是跟「資料」與「畫面」有關
> 許多框架也都嘗試用自己的思路來解決這個問題

</br>


### 那 React 的思路是什麼？



React 希望在資料更新時，能夠直接重新渲染頁面，不用主動去探究是數據的哪部份發生變化，要對應去更新頁面哪一部分的 DOM。

但頁面重新渲染的成本可是更高，所以才需要 Virtual DOM 作為緩衝，透過資料更新後，重新繪製 Virtual DOM，與實體 DOM 進行 Diff最後再把差異部分 Patch 上去

這不僅修正了重新渲染的成本問題，也降低了 data 與 view 交互更新的複雜度，提高了 developer 的開發體驗。


</br>

但 React 是怎麼做到的呢？感覺起來是一個黑盒子啊！
而今天要講的主題，就是究竟 React 在幕後幫我們做了哪些事？
就讓我們一起來試著看穿 React 吧！


</br>


![](https://i.imgur.com/mk5KNcP.png)




</br>
</br>


![](https://i.imgur.com/XmFpOre.png)
![](https://i.imgur.com/6I0gncH.png)

</br>

- ### 1. React.createElement()：畫設計圖 / 更新設計圖
    - 在寫 JSX 時，就像在畫網頁的設計圖。
    - JSX 其實是語法糖，裡面是 React.createElement()。
    - React.createElement() 所產出的「成果」就是一張「設計圖」。
    - 這張「設計圖」紀錄了所有關於 DOM（也就是畫面）的資訊，本質上是個「記載 DOM 資訊的物件」。
    - 其實「這個設計圖物件」就可以算是某種意義上的「virtual DOM」了。



- ### 2. Reconciliation（render phase?）：比對與計算
    - 「每次都要全部重畫？你搞我啊？」（設計對白）。
    - 有個引擎會「比對」新的 virtual DOM 與舊的 virtual DOM。
    - 而這個「虛 DOM」與「實 DOM」同步的過程，就叫做「**Reconciliation**」。
    - React 有開發一個 Reconciliation 引擎，叫「**Fiber**」（React 16 起）。
    - 當中有用到厲害的演算法。
    - 這個過程算是 React 的精髓。


- ### 3. Commit phase：實際畫到網頁上
    - 在上一步 React 的引擎幫我們比對完、判斷完之後，才實際更新「必要的」網頁 DOM 結構。
    - 執行各種 useEffect 裡的 function（打 API 之類的）。


</br>


![](https://i.imgur.com/G418tFc.png)


「virtual DOM」是一種程式撰寫的概念，比較像「pattern」（模式），而不是指一個特定的技術，所以當大家說 virtual DOM 時可能是在講不同的東西。

而在 React 當中，「virtual DOM」這個詞通常跟「**React elements**」有關，因為它們是「**記載了 UI 資訊的物件**」。

> In React world, the term “virtual DOM” is usually associated with React elements since they are the objects representing the user interface.

</br>

### 如果把 JSX / React.createElement() 產生的元素印出來：
![](https://i.imgur.com/AxuiNFc.png)


</br>

其中那些「記載 UI 資訊（的物件）」會被儲存起來，後續會再經過比對、計算，再透過像「ReactDOM」這樣的函式庫，去跟「真正的 DOM」同步。而樣的過程叫做「**Reconciliation**」。

</br>

### 但是...?

</br>

其實 React 官方文件有說，「virtual DOM」其實是不太準確的說法，因為 React 的核心引擎在於「**比對計算**」，計算完的結果則可以：
- 給「**React DOM**」render 到網頁上。
- 給「**React Native**」render 到 APP 上。


**reconciliation 的部分共用，然後 rendering 的部分則可以在各自環境執行。**

參考資料：https://github.com/acdlite/react-fiber-architecture

![](https://i.imgur.com/cfUQnOy.png)




</br></br>

### 其他小補充：

關於自幹一個 Virtual DOM：
- 從頭到尾打造一個簡單的 Virtual DOM：https://blog.techbridge.cc/2019/02/04/vdom-from-scratch/
- build your own react：https://pomb.us/build-your-own-react/

</br>
</br>


---

</br>

![](https://i.imgur.com/TIlRYgD.png)

![](https://i.imgur.com/pXcIHUc.png)


- 聲明式 API，而非指令式。不用關注 component 更新時底層有什麼改變。
- 這讓開發程式簡單許多！

</br>






![](https://i.imgur.com/CHQiCsW.png)


### 🌲 「舊 tree」 vs 「新 tree」

在使用 React 時，每次呼叫 render() 函式，我們都可以當成是建立了一顆由 React element 構成的樹狀結構。而在每一次有 state 或 props 更新時，render() 函式就會 return 一顆不同的 tree。

因此，React 需要判斷如何有效率的把 UI 從「舊的 tree」更新成「新的 tree」。


</br>

### 啓發式 (heuristic) 演算法：

> 翻譯：不是最完美滴，但是最有效率的

對於「如何用最少操作去將舊的 tree 轉換成新的 tree」的演算法問題有一些通用的解法，但即使是目前最先進的演算法都還需要 O(n3) 的時間複雜度（n 為 tree 中 element 的數量）。

React 會去比較更新前後的 Virtual DOM，並從而找到需要更新的「最小操作」，來減少多次操作 DOM 的成本。

假設 React 使用這種演算法，則呈現 1000 個 element 需要 10 億次的比較。因為這個比較成本實在太高，所以 React 在以下兩個假設下採用了一個 O(n) 的啓發式 (heuristic) 演算法：


</br>

### 兩個重要假設：

1. 只會比較同一層級的節點。兩個不同類型的 element 會產生出不同的 tree。
2. 可以通過設定 key 來指出哪些子 element 在不同的 render 下能保持不變。


</br>


### 用 Diffing 演算法的比對情況：

當比對兩顆 tree 時，React 首先比較兩棵 tree 的 root element：

![](https://i.imgur.com/JetmNK7.png)

</br>

### #1 比對不同類型的 Element

當比對的兩個 root element 為不同類型的元素時，React 會將原有的 tree 整顆拆掉並且重新建立起新的 tree。例如，當一個元素從` <a> `變成 `<img>`、從 `<Article>` 變成 `<Comment>`、或從 `<Button> `變成` <div> `時，都會觸發一個完整的重建流程。

</br>

### #2 比對同一類型的 DOM Element

當比對兩個相同類型的 React element 時，React 會保留 DOM點，只比對及更新有改變的 attribute。

</br>

### #3 對 Children 進行遞迴處理

在預設條件下，當遞迴處理 DOM 節點的 children 時，React 只會同時遍歷兩個 children 的 array，並在發現差異時，產生一個 mutation。

例如，當在 children 的 array 尾端新增一個 element 時，在這兩個 tree 之間的轉換效果很好：

React 會先匹配兩個 `<li>first</li>` 對應的 tree，然後匹配第二個元素 `<li>second</li>` 對應的 tree，最後插入第三個元素` <li>third</li>` 的 tree。

如果只是單純的實作，則在 array 開頭插入新元素會讓效能變差。例如：

</br>


### #4 Keys

為了解決以上問題，React 提供了 key 屬性。當 children 擁有 key 屬性時，React 使用 key 來匹配原有 tree 上的 children 以及後續 tree 的 children。例如，以下範例在新增 key 屬性之後，可以改善在上個例子中發生的效能問題：

補充：這個 key 不需要是全域唯一，但在 array 中需要保持唯一。

當使用 array 索引值作為 key 的 component 進行重新排序時，component state 可能會遇到一些問題。由於 component instance 是基於它們的 key 來決定是否更新以及重複使用，如果 key 是一個索引值，那麼修改順序時會修改目前的 key，導致 component 的 state（例如不受控制輸入框）可能相互篡改導致無法預期的變動。

由於 React 依賴啓發式的演算法，因此當以下假設沒有得到滿足，效能將會有所影響。

1. 該演算法不會嘗試匹配不同 component 類型的 subtree。如果你發現你在兩種不同類型的 component 中切換，但輸出非常相似的內容，建議把它們改成同一類型。實際上，我們沒有發現在改成同一種類型後會發生問題。
2. Key 應該具有穩定、可預測、以及 array 內唯一的特質。不穩定的 key（例如透過 Math.random() 隨機生成的）會導致許多 component instance 和 DOM 節點被不必要地重新建立，這可能導致效能低下和 child component 中的 state 丟失。





</br>



### 補充：Fiber：React 的 reconciliation engine


> Fiber is the new **reconciliation** **engine** in React 16. Its main goal is to enable incremental rendering of the virtual DOM
> 


![](https://i.imgur.com/YFgfNGl.png)





</br>

### 小結 #1：

- 可以先姑且用 virtual DOM 去理解 react，但跟實際上可能會有小差異。
- 流程：
    - 1. 畫 / 更新設計圖。
    - 2. Reconciliation 比對計算，
    - 3. 實際更新頁面 DOM。
- 承上，感覺 React 特別想把「2. 比對計算」跟「3. 實際更新頁面」這兩件事分開。




</br>

### 小結 #2：程式要怎麼寫，效能比較好？
- 想讓 DOM 有變化時，可以的話不要換標籤名稱（例如 div 換成 h1），會造成以下整個孩子孫子的 dom element 被銷毀重新 render。
- 插入小孩元素到爸媽元素之下時，從後面插隊，比從前面插隊，效能比較好。
    - 不然就是要好好用 key，讓 React 可以清楚比對哪個小孩要換、哪個不要換。
    - key 要是穩定的值，不然還是一樣會影響效能，或發生 bug。

</br>



![](https://i.imgur.com/j9pN0wN.png)

