這篇文章是基於 React 官方文檔對於 Virtual DOM 的理念和 Diffing 算法的策略的整合。html
Virtual DOM 是一種編程理念。UI 信息被特定語言描述並保存到內存中,再經過特定的庫,例如 ReactDOM 與真實的 DOM 同步信息。這一過程成爲 協調 (Reconciliation)
。node
Virtual DOM 反映到實際的數據結構上,就是每個 React 的 fiber nodereact
// UI 組件描述 const Span = (props) => <span></span> // 實際的 Fiber node structure { stateNode: new HTMLSpanElement, type: "span", alternate: null, key: null, updateQueue: null, memoizedState: null, pendingProps: {}, memoizedProps: {}, tag: 1, effectTag: 0, nextEffect: null }
這一抽離結構有點像 React 版本的 AST 抽象語法樹。算法
在 Virtual DOM -> Real DOM 之間的轉換過程當中,須要高效率的算法來支撐。因爲某個時刻調用 React render() 方法生成的 React 元素組成的樹,與下一次 state 或 props 變化時調用同一個 render 返回的樹是不同的,React 須要根據這兩個不一樣的樹來決定如何高效地讓最新的 Virtual DOM 反應到真實 DOM 中。編程
Diffing 算法就是解決如何更有效率地更新 UI 的關鍵。數組
React 採起了一個複雜度爲 O(n) 的比較策略,這個策略有兩個假設數據結構
若是爲不一樣類型,React 將會把原有的樹拆卸並從新創建新的樹。例如 <div>
-> <span>
。ide
在根節點如下的組件也會被卸載,它們的狀態會被銷燬。例如:函數
<div> <Counter /> </div> <span> <Counter /> </span>
當對比兩個同類型的 React 元素時,React 會保留 DOM 節點,僅對比以及更新有變化的屬性性能
<div className="before" title="stuff" /> <div className="after" title="stuff" />
經過對比兩個元素,React 得知 className
變化,因此只須要更新 DOM 對應元素上的 class
。
當處理完當前節點時,React 將會對子節點進行遞歸。
當一個 React 組件須要更新時(例如 props
有變化),組件實例保持不變,實例中的 state 能在不一樣渲染時保持一致。React 將更新該組件實例的 props
以保持與最新的元素的一致。並調用 該實例的原型 上的函數 getDerivedStateFromProps
(官方文檔是 componentWillReceiveProps 和 componentWillUpdate,但這將會被棄用)。
下一步是調用該實例的 render
方法,diffing 算法將在以前的結果和最新的結果中進行遞歸。
在默認條件下,當遞歸 DOM 節點的子元素時,React 會同時遍歷兩個子元素的列表,當發現兩個子元素有差別時,將生成一個「變種(mutation)」。
例如在子元素列表末尾新增元素時,更變開銷比較小。好比:
// before <ul> <li>first</li> <li>second</li> </ul> // after <ul> <li>first</li> <li>second</li> <li>third</li> </ul>
React 會匹配兩個 <li>first</li>
對應的樹、兩個 <li>second</li>
對應的樹,而後插入 <li>third</li>
樹。
但若是就這樣簡單實現的話,那麼在列表頭部插入會很影響性能,更變的開銷會比較大。好比:
<ul> <li>Duke</li> <li>Villanova</li> </ul> <ul> <li>Connecticut</li> <li>Duke</li> <li>Villanova</li> </ul>
React 會認爲每一個子元素都「改變(mutate)」了,而不會認爲能夠保持 <li>Duke</li>
和 <li>Villanova</li>
子樹不變,從而致使從新渲染。這種狀況下的低效可能會帶來性能問題。
爲了解決以上問題,React 支持 key 屬性。當子傳入 key 到子元素時,React 經過 key 來匹配比較原有樹上的子元素
以及最新樹上的子元素
的差別。如下例子在新增 key 以後使得以前的低效轉換變得高效:
<ul> <li key="2015">Duke</li> <li key="2016">Villanova</li> </ul> <ul> <li key="2014">Connecticut</li> <li key="2015">Duke</li> <li key="2016">Villanova</li> </ul>
如今 React 知道只有帶着 '2014' key 的元素是新元素,帶着 '2015' 以及 '2016' key 的元素僅僅移動了位置。
因此通常在開發的時候最好使用一個有惟一屬性的 id 來做爲 key
<li key={item.id}>{item.name}</li>
在開發者本身肯定數組數據不會輕易改變
的狀況下才能夠用數組下表來做爲 key。
上述只是 協調算法(reconciliation algorithm)的實現細節而已。React 能夠響應每一次 action 後從新渲染整個應用,最終結果也會是同樣的。
須要明確知道的是,在當前上下文(this context)
中從新渲染(rerender)
意味着會調用全部的 component
的 render()
,但並不意味着 React 會卸載(unmount)
或重載(remount)
它們。它(協調算法)只會用上述規則在其過程當中找出不一樣。