上一篇介绍了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