# 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)