目前,前端領域中 React 勢頭正盛,使用者衆多卻少有可以深刻剖析內部實現機制和原理。本系列文章但願經過剖析 React 源碼,理解其內部的實現原理,知其然更要知其因此然。javascript
React diff 做爲 Virtual DOM 的加速器,其算法上的改進優化是 React 整個界面渲染的基礎,以及性能提升的保障,同時也是 React 源碼中最神祕、最難以想象的部分,本文從源碼入手,深刻剖析 React diff 的難以想象之處。html
閱讀本文須要對 React 有必定的瞭解,若是你不知何爲 React,請詳讀 React 官方文檔。前端
若是你對 React diff 存在些許疑惑,或者你對算法優化感興趣,那麼本文值得閱讀和討論。java
React 中最值得稱道的部分莫過於 Virtual DOM 與 diff 的完美結合,特別是其高效的 diff 算法,讓用戶能夠無需顧忌性能問題而」任性自由」的刷新頁面,讓開發者也能夠無需關心 Virtual DOM 背後的運做原理,由於 React diff 會幫助咱們計算出 Virtual DOM 中真正變化的部分,並只針對該部分進行實際 DOM 操做,而非從新渲染整個頁面,從而保證了每次操做更新後頁面的高效渲染,所以 Virtual DOM 與 diff 是保證 React 性能口碑的幕後推手。react
行文至此,可能會有讀者質疑:React 無非就是引入 diff 這一律念,且 diff 算法也並不是其獨創,何須吹噓的如此天花亂墜呢?git
其實,正是由於 diff 算法的普識度高,就更應該承認 React 針對 diff 算法優化所作的努力與貢獻,更能體現 React 開發者們的魅力與智慧!github
計算一棵樹形結構轉換成另外一棵樹形結構的最少操做,是一個複雜且值得研究的問題。傳統 diff 算法經過循環遞歸對節點進行依次對比,效率低下,算法複雜度達到 O(n3),其中 n 是樹中節點的總數。O(n3) 到底有多可怕,這意味着若是要展現1000個節點,就要依次執行上十億次的比較。這種指數型的性能消耗對於前端渲染場景來講代價過高了!現今的 CPU 每秒鐘能執行大約30億條指令,即使是最高效的實現,也不可能在一秒內計算出差別狀況。算法
所以,若是 React 只是單純的引入 diff 算法而沒有任何的優化改進,那麼其效率是遠遠沒法知足前端渲染所要求的性能。性能
經過下面的 demo 能夠清晰的描述傳統 diff 算法的實現過程。優化
let result = []; // 比較葉子節點 const diffLeafs = function(beforeLeaf, afterLeaf) { // 獲取較大節點樹的長度 let count = Math.max(beforeLeaf.children.length, afterLeaf.children.length); // 循環遍歷 for (let i = 0; i < count; i++) { const beforeTag = beforeLeaf.children[i]; const afterTag = afterLeaf.children[i]; // 添加 afterTag 節點 if (beforeTag === undefined) { result.push({type: "add", element: afterTag}); // 刪除 beforeTag 節點 } else if (afterTag === undefined) { result.push({type: "remove", element: beforeTag}); // 節點名改變時,刪除 beforeTag 節點,添加 afterTag 節點 } else if (beforeTag.tagName !== afterTag.tagName) { result.push({type: "remove", element: beforeTag}); result.push({type: "add", element: afterTag}); // 節點不變而內容改變時,改變節點 } else if (beforeTag.innerHTML !== afterTag.innerHTML) { if (beforeTag.children.length === 0) { result.push({ type: "changed", beforeElement: beforeTag, afterElement: afterTag, html: afterTag.innerHTML }); } else { // 遞歸比較 diffLeafs(beforeTag, afterTag); } } } return result; }
所以,若是想要將 diff 思想引入 Virtual DOM,就須要設計一種穩定高效的 diff 算法,而 React 作到了!
那麼,React diff 究竟是如何實現的呢?
傳統 diff 算法的複雜度爲 O(n3),顯然這是沒法知足性能要求的。React 經過制定大膽的策略,將 O(n3) 複雜度的問題轉換成 O(n) 複雜度的問題。
Web UI 中 DOM 節點跨層級的移動操做特別少,能夠忽略不計。
擁有相同類的兩個組件將會生成類似的樹形結構,擁有不一樣類的兩個組件將會生成不一樣的樹形結構。
對於同一層級的一組子節點,它們能夠經過惟一 id 進行區分。
基於以上三個前提策略,React 分別對 tree diff、component diff 以及 element diff 進行算法優化,事實也證實這三個前提策略是合理且準確的,它保證了總體界面構建的性能。
tree diff
component diff
element diff
本文中源碼 ReactMultiChild.js
基於策略一,React 對樹的算法進行了簡潔明瞭的優化,即對樹進行分層比較,兩棵樹只會對同一層次的節點進行比較。
既然 DOM 節點跨層級的移動操做少到能夠忽略不計,針對這一現象,React 經過 updateDepth 對 Virtual DOM 樹進行層級控制,只會對相同顏色方框內的 DOM 節點進行比較,即同一個父節點下的全部子節點。當發現節點已經不存在,則該節點及其子節點會被徹底刪除掉,不會用於進一步的比較。這樣只須要對樹進行一次遍歷,便能完成整個 DOM 樹的比較。
updateChildren: function(nextNestedChildrenElements, transaction, context) { updateDepth++; var errorThrown = true; try { this._updateChildren(nextNestedChildrenElements, transaction, context); errorThrown = false; } finally { updateDepth--; if (!updateDepth) { if (errorThrown) { clearQueue(); } else { processQueue(); } } } }
分析至此,大部分人可能都存在這樣的疑問:若是出現了 DOM 節點跨層級的移動操做,React diff 會有怎樣的表現呢?是的,對此我也好奇不已,不如試驗一番。
以下圖,A 節點(包括其子節點)整個被移動到 D 節點下,因爲 React 只會簡單的考慮同層級節點的位置變換,而對於不一樣層級的節點,只有建立和刪除操做。當根節點發現子節點中 A 消失了,就會直接銷燬 A;當 D 發現多了一個子節點 A,則會建立新的 A(包括子節點)做爲其子節點。此時,React diff 的執行狀況:create A -> create B -> create C -> delete A。
由此可發現,當出現節點跨層級移動時,並不會出現想象中的移動操做,而是以 A 爲根節點的樹被整個從新建立,這是一種影響 React 性能的操做,所以 React 官方建議不要進行 DOM 節點跨層級的操做。
注意:在開發組件時,保持穩定的 DOM 結構會有助於性能的提高。例如,能夠經過 CSS 隱藏或顯示節點,而不是真的移除或添加 DOM 節點。
React 是基於組件構建應用的,對於組件間的比較所採起的策略也是簡潔高效。
若是是同一類型的組件,按照原策略繼續比較 virtual DOM tree。
若是不是,則將該組件判斷爲 dirty component,從而替換整個組件下的全部子節點。
對於同一類型的組件,有可能其 Virtual DOM 沒有任何變化,若是可以確切的知道這點那能夠節省大量的 diff 運算時間,所以 React 容許用戶經過 shouldComponentUpdate()
來判斷該組件是否須要進行 diff。
以下圖,當 component D 改變爲 component G 時,即便這兩個 component 結構類似,一旦 React 判斷 D 和 G 是不一樣類型的組件,就不會比較兩者的結構,而是直接刪除 component D,從新建立 component G 以及其子節點。雖然當兩個 component 是不一樣類型但結構類似時,React diff 會影響性能,但正如 React 官方博客所言:不一樣類型的 component 是不多存在類似 DOM tree 的機會,所以這種極端因素很難在實現開發過程當中形成重大影響的。
當節點處於同一層級時,React diff 提供了三種節點操做,分別爲:INSERT_MARKUP(插入)、MOVE_EXISTING(移動)和 REMOVE_NODE(刪除)。
INSERT_MARKUP,新的 component 類型不在老集合裏, 便是全新的節點,須要對新節點執行插入操做。
MOVE_EXISTING,在老集合有新 component 類型,且 element 是可更新的類型,generateComponentChildren 已調用 receiveComponent,這種狀況下 prevChild=nextChild,就須要作移動操做,能夠複用之前的 DOM 節點。
REMOVE_NODE,老 component 類型,在新集合裏也有,但對應的 element 不一樣則不能直接複用和更新,須要執行刪除操做,或者老 component 不在新集合裏的,也須要執行刪除操做。
function enqueueInsertMarkup(parentInst, markup, toIndex) { updateQueue.push({ parentInst: parentInst, parentNode: null, type: ReactMultiChildUpdateTypes.INSERT_MARKUP, markupIndex: markupQueue.push(markup) - 1, content: null, fromIndex: null, toIndex: toIndex, }); } function enqueueMove(parentInst, fromIndex, toIndex) { updateQueue.push({ parentInst: parentInst, parentNode: null, type: ReactMultiChildUpdateTypes.MOVE_EXISTING, markupIndex: null, content: null, fromIndex: fromIndex, toIndex: toIndex, }); } function enqueueRemove(parentInst, fromIndex) { updateQueue.push({ parentInst: parentInst, parentNode: null, type: ReactMultiChildUpdateTypes.REMOVE_NODE, markupIndex: null, content: null, fromIndex: fromIndex, toIndex: null, }); }
以下圖,老集合中包含節點:A、B、C、D,更新後的新集合中包含節點:B、A、D、C,此時新老集合進行 diff 差別化對比,發現 B != A,則建立並插入 B 至新集合,刪除老集合 A;以此類推,建立並插入 A、D 和 C,刪除 B、C 和 D。
React 發現這類操做繁瑣冗餘,由於這些都是相同的節點,但因爲位置發生變化,致使須要進行繁雜低效的刪除、建立操做,其實只要對這些節點進行位置移動便可。
針對這一現象,React 提出優化策略:容許開發者對同一層級的同組子節點,添加惟一 key 進行區分,雖然只是小小的改動,性能上卻發生了翻天覆地的變化!
新老集合所包含的節點,以下圖所示,新老集合進行 diff 差別化對比,經過 key 發現新老集合中的節點都是相同的節點,所以無需進行節點刪除和建立,只須要將老集合中節點的位置進行移動,更新爲新集合中節點的位置,此時 React 給出的 diff 結果爲:B、D 不作任何操做,A、C 進行移動操做,便可。
那麼,如此高效的 diff 究竟是如何運做的呢?讓咱們經過源碼進行詳細分析。
首先對新集合的節點進行循環遍歷,for (name in nextChildren)
,經過惟一 key 能夠判斷新老集合中是否存在相同的節點,if (prevChild === nextChild)
,若是存在相同節點,則進行移動操做,但在移動前須要將當前節點在老集合中的位置與 lastIndex 進行比較,if (child._mountIndex < lastIndex)
,則進行節點移動操做,不然不執行該操做。這是一種順序優化手段,lastIndex 一直在更新,表示訪問過的節點在老集合中最右的位置(即最大的位置),若是新集合中當前訪問的節點比 lastIndex 大,說明當前訪問節點在老集合中就比上一個節點位置靠後,則該節點不會影響其餘節點的位置,所以不用添加到差別隊列中,即不執行移動操做,只有當訪問的節點比 lastIndex 小時,才須要進行移動操做。
以上圖爲例,能夠更爲清晰直觀的描述 diff 的差別對比過程:
重新集合中取得 B,判斷老集合中存在相同節點 B,經過對比節點位置判斷是否進行移動操做,B 在老集合中的位置 B._mountIndex = 1
,此時 lastIndex = 0
,不知足 child._mountIndex < lastIndex
的條件,所以不對 B 進行移動操做;更新 lastIndex = Math.max(prevChild._mountIndex, lastIndex)
,其中 prevChild._mountIndex 表示 B 在老集合中的位置,則 lastIndex = 1,並將 B 的位置更新爲新集合中的位置 prevChild._mountIndex = nextIndex
,此時新集合中 B._mountIndex = 0
,nextIndex++
進入下一個節點的判斷。
重新集合中取得 A,判斷老集合中存在相同節點 A,經過對比節點位置判斷是否進行移動操做,A 在老集合中的位置 A._mountIndex = 0
,此時 lastIndex = 1
,知足 child._mountIndex < lastIndex
的條件,所以對 A 進行移動操做enqueueMove(this, child._mountIndex, toIndex)
,其中 toIndex 其實就是 nextIndex,表示 A 須要移動到的位置;更新 lastIndex = Math.max(prevChild._mountIndex, lastIndex)
,則 lastIndex = 1
,並將 A 的位置更新爲新集合中的位置 prevChild._mountIndex = nextIndex
,此時新集合中 A._mountIndex = 1
,nextIndex++
進入下一個節點的判斷。
重新集合中取得 D,判斷老集合中存在相同節點 D,經過對比節點位置判斷是否進行移動操做,D 在老集合中的位置 D._mountIndex = 3
,此時 lastIndex = 1
,不知足 child._mountIndex < lastIndex
的條件,所以不對 D 進行移動操做;更新 lastIndex = Math.max(prevChild._mountIndex, lastIndex)
,則 lastIndex = 3
,並將 D 的位置更新爲新集合中的位置 prevChild._mountIndex = nextIndex
,此時新集合中 D._mountIndex = 2
,nextIndex++
進入下一個節點的判斷。
重新集合中取得 C,判斷老集合中存在相同節點 C,經過對比節點位置判斷是否進行移動操做,C 在老集合中的位置 C._mountIndex = 2
,此時 lastIndex = 3
,知足 child._mountIndex < lastIndex
的條件,所以對 C 進行移動操做 enqueueMove(this, child._mountIndex, toIndex)
;更新 lastIndex = Math.max(prevChild._mountIndex, lastIndex)
,則 lastIndex = 3
,並將 C 的位置更新爲新集合中的位置 prevChild._mountIndex = nextIndex
,此時新集合中 A._mountIndex = 3
,nextIndex++
進入下一個節點的判斷,因爲 C 已是最後一個節點,所以 diff 到此完成。
以上主要分析新老集合中存在相同節點但位置不一樣時,對節點進行位置移動的狀況,若是新集合中有新加入的節點且老集合存在須要刪除的節點,那麼 React diff 又是如何對比運做的呢?
如下圖爲例:
重新集合中取得 B,判斷老集合中存在相同節點 B,因爲 B 在老集合中的位置 B._mountIndex = 1
,此時 lastIndex = 0
,所以不對 B 進行移動操做;更新 lastIndex = 1
,並將 B 的位置更新爲新集合中的位置 B._mountIndex = 0
,nextIndex++
進入下一個節點的判斷。
重新集合中取得 E,判斷老集合中不存在相同節點 E,則建立新節點 E;更新 lastIndex = 1
,並將 E 的位置更新爲新集合中的位置,nextIndex++
進入下一個節點的判斷。
重新集合中取得 C,判斷老集合中存在相同節點 C,因爲 C 在老集合中的位置C._mountIndex = 2
,此時 lastIndex = 1
,所以對 C 進行移動操做;更新 lastIndex = 2
,並將 C 的位置更新爲新集合中的位置,nextIndex++
進入下一個節點的判斷。
重新集合中取得 A,判斷老集合中存在相同節點 A,因爲 A 在老集合中的位置A._mountIndex = 0
,此時 lastIndex = 2
,所以不對 A 進行移動操做;更新 lastIndex = 2
,並將 A 的位置更新爲新集合中的位置,nextIndex++
進入下一個節點的判斷。
當完成新集合中全部節點 diff 時,最後還須要對老集合進行循環遍歷,判斷是否存在新集合中沒有但老集合中仍存在的節點,發現存在這樣的節點 D,所以刪除節點 D,到此 diff 所有完成。
_updateChildren: function(nextNestedChildrenElements, transaction, context) { var prevChildren = this._renderedChildren; var nextChildren = this._reconcilerUpdateChildren( prevChildren, nextNestedChildrenElements, transaction, context ); if (!nextChildren && !prevChildren) { return; } var name; var lastIndex = 0; var nextIndex = 0; for (name in nextChildren) { if (!nextChildren.hasOwnProperty(name)) { continue; } var prevChild = prevChildren && prevChildren[name]; var nextChild = nextChildren[name]; if (prevChild === nextChild) { // 移動節點 this.moveChild(prevChild, nextIndex, lastIndex); lastIndex = Math.max(prevChild._mountIndex, lastIndex); prevChild._mountIndex = nextIndex; } else { if (prevChild) { lastIndex = Math.max(prevChild._mountIndex, lastIndex); // 刪除節點 this._unmountChild(prevChild); } // 初始化並建立節點 this._mountChildAtIndex( nextChild, nextIndex, transaction, context ); } nextIndex++; } for (name in prevChildren) { if (prevChildren.hasOwnProperty(name) && !(nextChildren && nextChildren.hasOwnProperty(name))) { this._unmountChild(prevChildren[name]); } } this._renderedChildren = nextChildren; }, // 移動節點 moveChild: function(child, toIndex, lastIndex) { if (child._mountIndex < lastIndex) { this.prepareToManageChildren(); enqueueMove(this, child._mountIndex, toIndex); } }, // 建立節點 createChild: function(child, mountImage) { this.prepareToManageChildren(); enqueueInsertMarkup(this, mountImage, child._mountIndex); }, // 刪除節點 removeChild: function(child) { this.prepareToManageChildren(); enqueueRemove(this, child._mountIndex); }, _unmountChild: function(child) { this.removeChild(child); child._mountIndex = null; }, _mountChildAtIndex: function( child, index, transaction, context) { var mountImage = ReactReconciler.mountComponent( child, transaction, this, this._nativeContainerInfo, context ); child._mountIndex = index; this.createChild(child, mountImage); },
固然,React diff 仍是存在些許不足與待優化的地方,以下圖所示,若新集合的節點更新爲:D、A、B、C,與老集合對比只有 D 節點移動,而 A、B、C 仍然保持原有的順序,理論上 diff 應該只需對 D 執行移動操做,然而因爲 D 在老集合的位置是最大的,致使其餘節點的 _mountIndex < lastIndex
,形成 D 沒有執行移動操做,而是 A、B、C 所有移動到 D 節點後面的現象。
在此,讀者們能夠討論思考:如何優化上述問題?
建議:在開發過程當中,儘可能減小相似將最後一個節點移動到列表首部的操做,當節點數量過大或更新操做過於頻繁時,在必定程度上會影響 React 的渲染性能。
React 經過制定大膽的 diff 策略,將 O(n3) 複雜度的問題轉換成 O(n) 複雜度的問題;
React 經過分層求異的策略,對 tree diff 進行算法優化;
React 經過相同類生成類似樹形結構,不一樣類生成不一樣樹形結構的策略,對 component diff 進行算法優化;
React 經過設置惟一 key的策略,對 element diff 進行算法優化;
建議,在開發組件時,保持穩定的 DOM 結構會有助於性能的提高;
建議,在開發過程當中,儘可能減小相似將最後一個節點移動到列表首部的操做,當節點數量過大或更新操做過於頻繁時,在必定程度上會影響 React 的渲染性能。
若是本文可以爲你解決些許關於 React diff 算法的疑惑,請點個贊吧!