上一篇介紹了VD
的是怎麼建立VD Tree
的和怎麼根據VD Tree
生成真實的DOM
。上一章連接。node
這一章主要是來梳理當咱們的VD
有變化的時候,它的diff
算法是怎麼去比較生成一個diff
對象的。react
Diff 算法是 VD
中最核心的一個算法。經過輸入初始狀態狀態A(VNode)和最終狀態B(VNode),經過計算,就能夠到獲得描述從A到B狀態的對象(VPatch),而後再根據這個描述對象,咱們就能知道哪些節點是須要新增的,哪些節點是須要刪除的,哪些節點只是屬性變化了須要更新的等等這些。git
根據github.com/Matt-Esch/v…的源碼來看,Diff 算法主要有三種狀況,分別是:github
如下文章,將前一個狀態稱爲A,變動後的狀態稱爲B。算法
function diff(a, b) {
var patch = { a: a }
walk(a, b, patch, 0)
return patch
}
複製代碼
整個diff的算法的入口就是上面列的函數,首席聲明瞭一個patch
對象,默認將前一個VD Tree
存起來,整個 patch
對象最終會被傳入到walk
函數,進行加工最終獲得VPatch
對象(描述各個節點的變化)。數組
function walk(a, b, patch, index) {
if (a === b) {
return
}
// 由於判斷子元素的時候,會遞歸調用這個函數,
// 會嘗試的去獲取這個下標是否以前計算過。
var apply = patch[index]
var applyClear = false
if (isThunk(a) || isThunk(b)) {
thunks(a, b, patch, index)
} else if (b == null) {
if (!isWidget(a)) {
clearState(a, patch, index)
apply = patch[index]
}
apply = appendPatch(apply, new VPatch(VPatch.REMOVE, a, b))
} else if (isVNode(b)) {
if (isVNode(a)) {
if (a.tagName === b.tagName &&
a.namespace === b.namespace &&
a.key === b.key) {
var propsPatch = diffProps(a.properties, b.properties)
if (propsPatch) {
apply = appendPatch(apply,
new VPatch(VPatch.PROPS, a, propsPatch))
}
apply = diffChildren(a, b, patch, apply, index)
} else {
apply = appendPatch(apply, new VPatch(VPatch.VNODE, a, b))
applyClear = true
}
} else {
apply = appendPatch(apply, new VPatch(VPatch.VNODE, a, b))
applyClear = true
}
} else if (isVText(b)) {
if (!isVText(a)) {
apply = appendPatch(apply, new VPatch(VPatch.VTEXT, a, b))
applyClear = true
} else if (a.text !== b.text) {
apply = appendPatch(apply, new VPatch(VPatch.VTEXT, a, b))
}
} else if (isWidget(b)) {
if (!isWidget(a)) {
applyClear = true
}
apply = appendPatch(apply, new VPatch(VPatch.WIDGET, a, b))
}
if (apply) {
patch[index] = apply
}
if (applyClear) {
clearState(a, patch, index)
}
}
複製代碼
代碼還算比較長,可是邏輯仍是比較清楚,下面來對每一個分之進行分析。app
A
和B
若是是全等,那就是節點一點都沒有變動,直接結束。if (a === b) {
return
}
複製代碼
A
或者B
被判斷爲Thunk
則使用Thunk
的比較方式。這裏最終仍是會調用diff
函數,回到節點的比較,中間會多幾層判斷。if (isThunk(a) || isThunk(b)) {
thunks(a, b, patch, index)
}
複製代碼
B
爲空,就會生成一個標爲REMOVE
的VPatch
對象。else if (b == null) {
// If a is a widget we will add a remove patch for it
// Otherwise any child widgets/hooks must be destroyed.
// This prevents adding two remove patches for a widget.
if (!isWidget(a)) {
clearState(a, patch, index)
apply = patch[index]
}
apply = appendPatch(apply, new VPatch(VPatch.REMOVE, a, b))
}
複製代碼
B
是一個VD
對象,接下來就開始進行比較:
A
也是一個VD
對象,經過比較tagName
、namespace
和key
。Props
,獲得props
的VPatch
對象(這個放到Props diff
分析),比較完props
以後,繼續比較child
子節點(放到後面講)VNODE
,也就是表示該節點標記爲替換。else if (isVNode(b)) {
if (isVNode(a)) {
if (a.tagName === b.tagName &&
a.namespace === b.namespace &&
a.key === b.key) {
var propsPatch = diffProps(a.properties, b.properties)
if (propsPatch) {
apply = appendPatch(apply,
new VPatch(VPatch.PROPS, a, propsPatch))
}
apply = diffChildren(a, b, patch, apply, index)
} else {
apply = appendPatch(apply, new VPatch(VPatch.VNODE, a, b))
applyClear = true
}
} else {
apply = appendPatch(apply, new VPatch(VPatch.VNODE, a, b))
applyClear = true
}
}
複製代碼
B
是文本節點,A
不是文本節點,那就標記當前節點爲VTEXT
也就是將當前節點替換成文本節點。若是A
也是文本節點,那就比較A
和B
節點的值,若是不一樣則標記替換文本節點。else if (isVText(b)) {
if (!isVText(a)) {
apply = appendPatch(apply, new VPatch(VPatch.VTEXT, a, b))
applyClear = true
} else if (a.text !== b.text) {
apply = appendPatch(apply, new VPatch(VPatch.VTEXT, a, b))
}
}
複製代碼
6.若是B
節點是Widget
,就將當前節點替換成Widget
元素,標記爲WIDGET
。dom
else if (isWidget(b)) {
if (!isWidget(a)) {
applyClear = true
}
apply = appendPatch(apply, new VPatch(VPatch.WIDGET, a, b))
}
複製代碼
patch[index]
中,apply
就是對當前節點變更的描述對象了。if (apply) {
patch[index] = apply
}
複製代碼
上面7個步驟就是VNode
的diff
算法,能夠看到,在B
爲VNode
的狀況下,還會去繼續比較B
和A
的屬性和子元素。函數
props的diff算法,文件地址,能夠看到,整個函數是一個for
循環,使用for in
循環來遍歷A
的屬性。源碼分析
function diffProps(a, b) {
var diff
for (var aKey in a) {
if (!(aKey in b)) {
diff = diff || {}
diff[aKey] = undefined
}
var aValue = a[aKey]
var bValue = b[aKey]
if (aValue === bValue) {
continue
} else if (isObject(aValue) && isObject(bValue)) {
if (getPrototype(bValue) !== getPrototype(aValue)) {
diff = diff || {}
diff[aKey] = bValue
} else if (isHook(bValue)) {
diff = diff || {}
diff[aKey] = bValue
} else {
var objectDiff = diffProps(aValue, bValue)
if (objectDiff) {
diff = diff || {}
diff[aKey] = objectDiff
}
}
} else {
diff = diff || {}
diff[aKey] = bValue
}
}
for (var bKey in b) {
if (!(bKey in a)) {
diff = diff || {}
diff[bKey] = b[bKey]
}
}
return diff
}
複製代碼
A
元素裏面的屬性在B
元素中已經不存在了,則將diff[aKey]
置爲undefined
,用來標記爲刪除。if (!(aKey in b)) {
diff = diff || {}
diff[aKey] = undefined
}
複製代碼
A
和B
裏面相同Key
的值,也就是當前遍歷的Key
對應的值。var aValue = a[aKey]
var bValue = b[aKey]
複製代碼
Key
。if (aValue === bValue) {
continue
}
複製代碼
A
和B
這兩個屬性都是對象,則繼續往下比較。
diff[aKey] = bValue
。B
的屬性是Hook
,則記錄diff[aKey] = bValue
。A
和B
的當前屬性,這兩個對象,獲得的diffObject
記錄到diff[aKey] = objectDiff
。經過這點能夠看到這個庫的props
的比較是深比較,會遞歸比較props
的每個Key
。else if (isObject(aValue) && isObject(bValue)) {
if (getPrototype(bValue) !== getPrototype(aValue)) {
diff = diff || {}
diff[aKey] = bValue
} else if (isHook(bValue)) {
diff = diff || {}
diff[aKey] = bValue
} else {
var objectDiff = diffProps(aValue, bValue)
if (objectDiff) {
diff = diff || {}
diff[aKey] = objectDiff
}
}
}
複製代碼
diff[aKey] = bValue
。else {
diff = diff || {}
diff[aKey] = bValue
}
複製代碼
B
中有可是A
總沒有的Key
,也就是新增的Key
,標記爲diff[bKey] = b[bKey]
。for (var bKey in b) {
if (!(bKey in a)) {
diff = diff || {}
diff[bKey] = b[bKey]
}
}
複製代碼
最後函數放回當前的diff
對象。
以前說過,child
的 diff
其實仍是會遞歸調用的 diff
函數,下面咱們來看看。
function diffChildren(a, b, patch, apply, index) {
var aChildren = a.children
var orderedSet = reorder(aChildren, b.children)
var bChildren = orderedSet.children
var aLen = aChildren.length
var bLen = bChildren.length
var len = aLen > bLen ? aLen : bLen
for (var i = 0; i < len; i++) {
var leftNode = aChildren[i]
var rightNode = bChildren[i]
index += 1
if (!leftNode) {
if (rightNode) {
// Excess nodes in b need to be added
apply = appendPatch(apply,
new VPatch(VPatch.INSERT, null, rightNode))
}
} else {
walk(leftNode, rightNode, patch, index)
}
if (isVNode(leftNode) && leftNode.count) {
index += leftNode.count
}
}
if (orderedSet.moves) {
// Reorder nodes last
apply = appendPatch(apply, new VPatch(
VPatch.ORDER,
a,
orderedSet.moves
))
}
return apply
}
複製代碼
A
和B
的child
放在一塊兒進行順序調整,方便以後能更好的比較。var aChildren = a.children
var orderedSet = reorder(aChildren, b.children)
var bChildren = orderedSet.children
複製代碼
var aLen = aChildren.length
var bLen = bChildren.length
var len = aLen > bLen ? aLen : bLen
複製代碼
for (var i = 0; i < len; i++) {
...
}
複製代碼
A
節點的當前子節點是不存在的,可是B
節點卻有。標記爲插入新節點。if (!leftNode) {
if (rightNode) {
// Excess nodes in b need to be added
apply = appendPatch(apply,
new VPatch(VPatch.INSERT, null, rightNode))
}
}
複製代碼
A
和B
兩個節點的當前子節點都是存在的,則遞歸調用walk
函數操做,注意這裏傳入的index
爲當前子節點的下標,這就是walk
函數中index
的來源了,主要是用來區分子元素的。else {
walk(leftNode, rightNode, patch, index)
}
複製代碼
ORDER
表示知識更換了順序。if (orderedSet.moves) {
// Reorder nodes last
apply = appendPatch(apply, new VPatch(
VPatch.ORDER,
a,
orderedSet.moves
))
}
複製代碼
這個函數就是上面第一步中,進行調整順序的函數,裏面會使用到咱們常常看到React
中說 同級節點須要添加的 key
。
// List diff, naive left to right reordering
function reorder(aChildren, bChildren) {
// O(M) time, O(M) memory
var bChildIndex = keyIndex(bChildren)
var bKeys = bChildIndex.keys
var bFree = bChildIndex.free
if (bFree.length === bChildren.length) {
return {
children: bChildren,
moves: null
}
}
// O(N) time, O(N) memory
var aChildIndex = keyIndex(aChildren)
var aKeys = aChildIndex.keys
var aFree = aChildIndex.free
if (aFree.length === aChildren.length) {
return {
children: bChildren,
moves: null
}
}
// O(MAX(N, M)) memory
var newChildren = []
var freeIndex = 0
var freeCount = bFree.length
var deletedItems = 0
// Iterate through a and match a node in b
// O(N) time,
for (var i = 0 ; i < aChildren.length; i++) {
var aItem = aChildren[i]
var itemIndex
if (aItem.key) {
if (bKeys.hasOwnProperty(aItem.key)) {
// Match up the old keys
itemIndex = bKeys[aItem.key]
newChildren.push(bChildren[itemIndex])
} else {
// Remove old keyed items
itemIndex = i - deletedItems++
newChildren.push(null)
}
} else {
// Match the item in a with the next free item in b
if (freeIndex < freeCount) {
itemIndex = bFree[freeIndex++]
newChildren.push(bChildren[itemIndex])
} else {
// There are no free items in b to match with
// the free items in a, so the extra free nodes
// are deleted.
itemIndex = i - deletedItems++
newChildren.push(null)
}
}
}
var lastFreeIndex = freeIndex >= bFree.length ?
bChildren.length :
bFree[freeIndex]
// Iterate through b and append any new keys
// O(M) time
for (var j = 0; j < bChildren.length; j++) {
var newItem = bChildren[j]
if (newItem.key) {
if (!aKeys.hasOwnProperty(newItem.key)) {
// Add any new keyed items
// We are adding new items to the end and then sorting them
// in place. In future we should insert new items in place.
newChildren.push(newItem)
}
} else if (j >= lastFreeIndex) {
// Add any leftover non-keyed items
newChildren.push(newItem)
}
}
var simulate = newChildren.slice()
var simulateIndex = 0
var removes = []
var inserts = []
var simulateItem
for (var k = 0; k < bChildren.length;) {
var wantedItem = bChildren[k]
simulateItem = simulate[simulateIndex]
// remove items
while (simulateItem === null && simulate.length) {
removes.push(remove(simulate, simulateIndex, null))
simulateItem = simulate[simulateIndex]
}
if (!simulateItem || simulateItem.key !== wantedItem.key) {
// if we need a key in this position...
if (wantedItem.key) {
if (simulateItem && simulateItem.key) {
// if an insert doesn't put this key in place, it needs to move
if (bKeys[simulateItem.key] !== k + 1) {
removes.push(remove(simulate, simulateIndex, simulateItem.key))
simulateItem = simulate[simulateIndex]
// if the remove didn't put the wanted item in place, we need to insert it
if (!simulateItem || simulateItem.key !== wantedItem.key) {
inserts.push({key: wantedItem.key, to: k})
}
// items are matching, so skip ahead
else {
simulateIndex++
}
}
else {
inserts.push({key: wantedItem.key, to: k})
}
}
else {
inserts.push({key: wantedItem.key, to: k})
}
k++
}
// a key in simulate has no matching wanted key, remove it
else if (simulateItem && simulateItem.key) {
removes.push(remove(simulate, simulateIndex, simulateItem.key))
}
}
else {
simulateIndex++
k++
}
}
// remove all the remaining nodes from simulate
while(simulateIndex < simulate.length) {
simulateItem = simulate[simulateIndex]
removes.push(remove(simulate, simulateIndex, simulateItem && simulateItem.key))
}
// If the only moves we have are deletes then we can just
// let the delete patch remove these items.
if (removes.length === deletedItems && !inserts.length) {
return {
children: newChildren,
moves: null
}
}
return {
children: newChildren,
moves: {
removes: removes,
inserts: inserts
}
}
}
複製代碼
這個函數有點長,仍是一步一步來梳理。
keyIndex
函數獲取bChildren
設置了key
和沒有設置key
的元素下標集合。若是都沒有設置key
就直接將bChildren
返回。var bChildIndex = keyIndex(bChildren)
var bKeys = bChildIndex.keys
var bFree = bChildIndex.free
if (bFree.length === bChildren.length) {
return {
children: bChildren,
moves: null
}
}
複製代碼
keyIndex
函數獲取aChildren
設置了key
和沒有設置key
的元素下標集合。若是都沒有設置key
就直接將bChildren
返回。var aChildIndex = keyIndex(aChildren)
var aKeys = aChildIndex.keys
var aFree = aChildIndex.free
if (aFree.length === aChildren.length) {
return {
children: bChildren,
moves: null
}
}
複製代碼
aChildren
,分爲兩種狀況。
aItem
存在key
,則根據key
去bChildren
的keys
集合中找,若是找的到,則將bChildren
對應的節點 push
到 newChildren
中。找不到則 push
一個 null
到 newChildren
。aItem
不存在key
,則去bChildren
中沒有keys
的集合中找第一個元素,將該元素 push
到 newChildren
中,若是已經找完了或者爲空,則 push
一個 null
到 newChildren
。if (aItem.key) {
if (bKeys.hasOwnProperty(aItem.key)) {
// Match up the old keys
itemIndex = bKeys[aItem.key]
newChildren.push(bChildren[itemIndex])
} else {
// Remove old keyed items
itemIndex = i - deletedItems++
newChildren.push(null)
}
} else {
// Match the item in a with the next free item in b
if (freeIndex < freeCount) {
itemIndex = bFree[freeIndex++]
newChildren.push(bChildren[itemIndex])
} else {
// There are no free items in b to match with
// the free items in a, so the extra free nodes
// are deleted.
itemIndex = i - deletedItems++
newChildren.push(null)
}
}
複製代碼
bChildren
,將對應在 aChildren
中沒有的 key
對應的元素或者尚未被添加到 newChildren
的剩下元素 push
到 newChildren
。for (var j = 0; j < bChildren.length; j++) {
var newItem = bChildren[j]
if (newItem.key) {
if (!aKeys.hasOwnProperty(newItem.key)) {
// Add any new keyed items
// We are adding new items to the end and then sorting them
// in place. In future we should insert new items in place.
newChildren.push(newItem)
}
} else if (j >= lastFreeIndex) {
// Add any leftover non-keyed items
newChildren.push(newItem)
}
}
複製代碼
newChildren
數組了,最後將bChildren和新數組逐個比較,獲得重新數組轉換到bChildren數組的move操做patch(即remove+insert)。for (var k = 0; k < bChildren.length;) {
...
if (!simulateItem || simulateItem.key !== wantedItem.key) {
...
...
removes.push(remove(simulate, simulateIndex, simulateItem.key))
simulateItem = simulate[simulateIndex]
...
...
}
}
複製代碼
chilren
數組和相關標記,新數組和move操做列表。這就能夠看到 diffChilren
裏面有 if (orderedSet.moves)
判斷,優化比較避免新增元素。更多能夠看React 源碼剖析系列 - 難以想象的 react diff。經過上面三部分的分析,發現 diff
算法就是按照 DOM
的描述來進行比較的,在比較 children
的時候會利用 key
來優化標記,避免重複建立新 DOM
。經過遞歸來比較 VD
獲得 VPatch
對象。
上一章說了 VD
樹的生成和 DOM
的建立,結合這章就能夠知道當數據和頁面變動後,VD
是怎麼去比較的,總體來講,梳理了一遍仍是明白了很多東西。
下一章就來梳理 patch
的過程了。
參考文章連接: 如何實現一個虛擬 DOM——virtual-dom 源碼分析 React 源碼剖析系列 - 難以想象的 react diff