1. 前言
diff並不是React獨創,只是React針對diff算法進行了優化。在React中diff算法和Virtual Dom的完美結合可讓咱們不須要關心頁面的變化致使的性能問題。由於diff會幫助咱們計算出Virtual Dom變化的部分,而後只對該部分進行原生DOM操做,而非從新渲染整個頁面,從而保證每次操做更新後頁面的高效渲染。node
2. 詳解diff
React將Virtual Dom轉爲真實DOM樹的最少操做的過程叫作調和(reconciliation)。diff即是調和的具體實現。算法
3. diff策略
- 策略一: Web UI中DOM節點跨層級的移動操做特別少,能夠忽略不計。
- 策略二: 擁有相同類的兩個組件將會生成類似的樹形結構,擁有不一樣類的兩個組件將會生成不一樣的樹形結構。
- 策略三: 對於同一層級的一組節點,它們能夠經過惟一id進行區分。 基於以上策略,React分別對tree diff、component diff、element diff進行算法優化。
4. tree diff 對於樹的比較
基於策略一( Web UI中DOM節點跨層級的移動操做特別少,能夠忽略不計)。React對樹的算法進行簡潔明瞭的優化,既對樹進行分層比較,兩棵樹只會對同一層次的節點進行比較。bash
既然DOM節點跨層級的移動操做能夠忽略不計,針對這一現象,React經過updateDepth對Virtual DOM樹進行層級控制,只會對相同層級的DOM進行比較,即同一個父節點下的全部子節點,當發現節點已經不存在時,則刪除該節點以及其子節點,不會用於進一步比較,這樣只需對樹進行一次遍歷,便可以完成整個DOM樹比較。源碼分析
updateChildren: function(nextNestedChildrenElements, transaction, context) {
updateDepth ++
var errorThrown = true
try {
this._updateChildren(nextNestedChildrenElements, transaction, context)
} finally {
updateDepth --
if(!updateDepth) {
if(errorThrown) {
clearQueue()
}else {
processQueue()
}
}
}
}
複製代碼
以下出現DOM節點跨層級移動操做,A節點整個被移動到了D節點下,因爲React只會簡單的考慮同層級節點的位置變化,對於不一樣層級的節點只有建立和刪除。當根節點發現子節點中A消失,就會直接銷燬A;當D發現多了一個子節點A,則會建立新的A和其子節點,此時diff執行狀況:createA--->createB--->crateC--->createA:性能
能夠發現跨層級移動時,會出現從新建立整個樹的操做,這種比較影響性能,因此不推薦進行DOM節點跨節點操做。
5. component diff 對於組件的比較
React是基於組件構建應用的,對於組件間的比較所採起的策略也是很是簡潔的、高效的。優化
- 若是是同一類型的組件,按照原策略繼續比較Virtual DOM樹便可。
- 若是不是同一類型組件,則將該組件判斷爲dirtycomponent,從而替換整個組件下的全部子組件。
- 對於同一類型的組件,有可能其Virtual DOM沒有任何變化,若是可以確切知道這點,那麼就能夠節省大量的diff運算時間。所以React 容許用戶經過shouldComponentUpdate來判斷該組件是否須要進行diff算法分析。
6. element diff
當節點處於同一層級時,diff提供了3種節點操做,分別爲INSERT_MARKUP(插入)、MOVE_EXISTING(移動)、REMOVE_NODE(刪除):ui
- INSERT_MARKUP:新的組件類型不在舊集合裏,即全新的節點,須要對新節點執行插入操做。
- MOVE_EXISTING:舊集合中有新組件類型,且element是可更新的類型,generateComponentChildren已調用receiveComponent,這種狀況下prevChild=nextChild,就須要作移動操做,能夠複用之前的DOM節點。
- REMOVE_NODE:舊組件類型,在新集合裏也有,但對應的element不一樣則不能直接複用和更新,須要執行刪除操做,或者舊組件不在新集合裏,也須要執行刪除操做。 相關代碼以下:
function makeInsertMarkup(markup, afterNode) {
return {
type: ReactMultiChildUpdateTypes.INSERT_MARKUP,
content: markup,
fromIndex: null,
fromNode: null,
toIndexL toIndex,
afterNode: afterNode
}
}
function makeMove(child, afterNode, toIndex) {
return {
type: ReactMultiChildUpdateTypes.MOVE)EXISTING,
content: null,
fromIndex: child._mountIndex,
fromNode: ReactReconciler.getNativeNode(child),
toIndex: toIndex,
afterNode: afterNode
}
}
function makeRemove(child, node) {
return {
type: ReactMultiChildUpdateTypes.REMOVE_NODE,
content: null,
fromIndex: child._mountIndex,
fromNode: node,
toIndex: null,
afterNode: null
}
}
複製代碼
以下圖:舊集合中包含節點A,B,C,D。更新後集閤中包含節點B,A,D,C。此時新舊集合進行diff差別化對比,發現B!=A,則建立並插入B至新集合,刪除就集合A。以此類推,建立並插入A,D,C。刪除B,C,D。this
React發現這類操做繁瑣冗餘,由於這些都是相同的節點,只是因爲位置發生變化,致使須要進行煩雜低效的刪除,建立操做,其實只要對這些節點進行位置移動便可。spa
針對這一現象,React提出優化策略:容許開發者對同一層級的同組子節點,添加惟一的key進行區別。3d
新舊集合所包含的節點以下圖所示,進行diff差別化對比後,經過key發現新舊集合中的節點都是相同的節點,所以無需進行節點的刪除和建立,只須要將舊集合中節點的位置進行移動,更爲新集合中節點的位置,此時React給出的diff結果爲:B,D不作任何操做,A,C進行移動便可。
源碼分析一波: 首先,對新集合中的節點進行循環遍歷,經過惟一的key判斷新舊集合中是否存在相同的節點,若是存在相同的節點,則進行移動操做,但在移動前須要將當前節點在舊集合中的位置與lastIndex進行比較。不然不執行該操做。這是一種順序優化手段,lastIndex一直更新,表示訪問過的節點在舊集合中最右邊的位置(即最大的位置)。若是新集合中當前訪問的節點比lastIndex大,說明當前訪問節點在舊集合中就比上一個節點位置靠後,則該節點不會影響其餘節點的位置,所以不用添加到差別隊列中,即不執行移動操做
以上面的那個圖爲例子:
- 重新集合中取得B, 判斷舊集合中是否存在相同節點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 = nextIdex,此時新集合中**B._mountIndex = 0, nextIndex ++**進入下一個節點判斷。
- 重新集合中取得A, 而後判斷舊集合中是否存在A相同節點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 在舊集合中的位置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 在舊集合中的位置 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 操做到此完成。
若是有新增的節點和刪除的節點diff如何處理呢?(如下都是複製的,碼不動字了....)
- 重新集合中取得B,而後判斷舊集合中存在是否相同節點 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 在舊集合中的位置 C._mountIndex = 2 , lastIndex = 1 ,此時 C._mountIndex > lastIndex ,所以不對 C 進行移動操做。更新 lastIndex = 2 ,並將 C 的位置更新爲新集 閤中的位置, nextIndex++ 進入下一個節點的判斷。
- 重新集合中取得 A,而後判斷舊集合中是否存在相同節點 A,此時發現存在節點 A。因爲 A 在舊集合中的位置 A._mountIndex = 0 , lastIndex = 2 ,此時 A._mountIndex < lastIndex , 所以對 A 進行移動操做。更新 lastIndex = 2 ,並將 A 的位置更新爲新集合中的位置, nextIndex++ 進入下一個節點的判斷。
- 當完成新集合中全部節點的差別化對比後,還須要對舊集合進行循環遍歷,判斷是否存 在新集合中沒有但舊集合中仍存在的節點,此時發現存在這樣的節點 D,所以刪除節點 D, 到此 diff 操做所有完成。
這一篇讀的有點亂...稍微總結一下下:
- React 經過diff 策略,將 O(n3) 複雜度的問題轉換成 O(n) 複雜度的問題;
- React 經過分層求異的策略,對 tree diff 進行算法優化;
- React 經過相同類生成類似樹形結構,不一樣類生成不一樣樹形結構的策略,對 component diff 進行算法優化;
- React 經過設置惟一 key的策略,對 element diff 進行算法優化;