概述
本文经由过程对virtual-dom的源码举行浏览和剖析,针对Virtual DOM的构造和相干的Diff算法举行解说,让读者能够对全部数据构造以及相干的Diff算法有肯定的相识。
Virtual DOM中Diff算法获得的结果怎样映射到实在DOM中,我们将在下一篇博客发表。
本文的主要内容为:
- Virtual DOM的构造
- Virtual DOM的Diff算法
注:这个Virtual DOM的完成并非React Virtual DOM的源码,而是基于virtual-dom)这个库。二者在原理上相似,而且这个库越发简朴轻易明白。相较于这个库,React对Virtual DOM做了进一步的优化和调解,我会在后续的博客中举行剖析。
Virtual DOM的构造
VirtualNode
作为Virtual DOM的元数据构造,VirtualNode位于vnode/vnode.js
文件中。我们截取一部份声明代码来看下内部构造:
function VirtualNode(tagName, properties, children, key, namespace) {
this.tagName = tagName
this.properties = properties || noProperties //props对象,Object范例
this.children = children || noChildren //子节点,Array范例
this.key = key != null ? String(key) : undefined
this.namespace = (typeof namespace === "string") ? namespace : null
...
this.count = count + descendants
this.hasWidgets = hasWidgets
this.hasThunks = hasThunks
this.hooks = hooks
this.descendantHooks = descendantHooks
}
VirtualNode.prototype.version = version //VirtualNode版本号,isVnode()检测标志
VirtualNode.prototype.type = "VirtualNode" // VirtualNode范例,isVnode()检测标志
上面就是一个VirtualNode的完全构造,包含了特定的标署名、属性、子节点等。
VText
VText是一个纯文本的节点,对应的是HTML中的纯文本。因而,这个属性也只要text
这一个字段。
function VirtualText(text) {
this.text = String(text)
}
VirtualText.prototype.version = version
VirtualText.prototype.type = "VirtualText"
VPatch
VPatch是示意须要对Virtual DOM实行的操纵纪录的数据构造。它位于vnode/vpatch.js
文件中。我们来看下内里的详细代码:
// 定义了操纵的常量,如Props变化,增添节点等
VirtualPatch.NONE = 0
VirtualPatch.VTEXT = 1
VirtualPatch.VNODE = 2
VirtualPatch.WIDGET = 3
VirtualPatch.PROPS = 4
VirtualPatch.ORDER = 5
VirtualPatch.INSERT = 6
VirtualPatch.REMOVE = 7
VirtualPatch.THUNK = 8
module.exports = VirtualPatch
function VirtualPatch(type, vNode, patch) {
this.type = Number(type) //操纵范例
this.vNode = vNode //须要操纵的节点
this.patch = patch //须要操纵的内容
}
VirtualPatch.prototype.version = version
VirtualPatch.prototype.type = "VirtualPatch"
个中常量定义了对VNode节点的操纵。比方:VTEXT就是增添一个VText节点,PROPS就是当前节点有Props属性转变。
Virtual DOM的Diff算法
相识了假造DOM中的三个构造,那我们下面来看下Virtual DOM的Diff算法。
这个Diff算法是Virtual DOM中最中心的一个算法。经由过程输入初始状况A(VNode)和终究状况B(VNode),这个算法能够获得从A到B的变化步骤(VPatch),依据获得的这一连串步骤,我们就能够晓得哪些节点须要新增,哪些节点须要删除,哪些节点的属性有了变化。在这个Diff算法中,又分成了三部份:
- VNode的Diff算法
- Props的Diff算法
- Vnode children的Diff算法
下面,我们就来一个一个引见这些Diff算法。
VNode的Diff算法
该算法是针对于单个VNode的比较算法。它是用于两个树中单个节点比较的场景。详细算法以下,假如不想直接浏览源码的同砚也能够翻到下面,会有相干代码流程申明供人人参考:
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 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))
} 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)
}
}
代码详细逻辑以下:
- 假如
a
和b
这两个VNode全等,则以为没有修正,直接返回。 - 假如个中有一个是thunk,则运用thunk的比较要领
thunks
。 - 假如
a
是widget且b
为空,那末经由过程递归将a
和它的子节点的remove
操纵添加到patch中。 假如
b
是VNode的话,- 假如
a
也是VNode,那末比较tagName
、namespace
、key
,假如雷同则比较两个VNode的Props(用下面提到的diffProps算法),同时比较两个VNode的children(用下面提到的diffChildren算法);假如差别则直接将b
节点的insert
操纵添加到patch中,同时将标记位置为true。 - 假如
a
不是VNode,那末直接将b
节点的insert
操纵添加到patch中,同时将标记位置为true。
- 假如
- 假如
b
是VText的话,看a
的范例是不是为VText,假如不是,则将VText
操纵添加到patch中,而且将标志位设置为true;假如是且文本内容差别,则将VText
操纵添加到patch中。 - 假如
b
是Widget的话,看a
的范例是不是为widget,假如是,将标志位设置为true。不管a
范例为何,都将Widget
操纵添加到patch中。 - 搜检标志位,假如标识为为true,那末经由过程递归将
a
和它的子节点的remove
操纵添加到patch中。
这就是单个VNode节点的diff算法全过程。这个算法是全部diff算法的进口,两棵树的比较就是从这个算法最先的。
Prpps的Diff算法
看完了单个VNode节点的diff算法,我们来看下上面提到的diffProps
算法。
该算法是针对于两个比较的VNode节点的Props比较算法。它是用于两个场景中key值和标署名都雷同的状况。详细算法以下,假如不想直接浏览源码的同砚也能够翻到下面,会有相干代码流程申明供人人参考:
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
对象。- 当key值不存在于
b
,则将此值存储下来,value赋值为undefined
。 - 当此key对应的两个属性都雷同时,继承停止此次轮回,举行下次轮回。
- 当key值对应的value差别且key值对应的两个value都是对象时,推断Prototype值,假如差别则纪录key对应的
b
对象的值;假如b
对应的value是hook
的话,纪录b的值。 - 上面前提推断都差别且都是对象时,则继承比较key值对应的两个对象(递归)。
- 当有一个不是对象时,直接将
b
对应的value举行纪录。
- 当key值不存在于
- 遍历
b
对象,将一切a
对象中不存在的key值对应的对象都纪录下来。
全部算法的大抵流程以下,由于比较简朴,就不画相干流程图了。假如逻辑有些绕的话,能够合营代码食用,结果更佳。
Vnode children的Diff算法
下面让我们来看下末了一个算法,就是关于两个VNode节点的children属性的diffChildren
算法。这个个diff算法分为两个部份,第一部份是将变化后的结果b
的children举行递次调解的算法,保证能够疾速的和a
的children举行比较;第二部份就是将a
的children与重新排序调解后的b
的children举行比较,获得相干的patch。下面,让我们一个一个算法来看。
reorder算法
该算法的作用是将b
节点的children数组举行调解重新排序,让a
和b
两个children之间的diff算法越发勤俭时候。详细代码以下:
function reorder(aChildren, bChildren) {
// O(M) time, O(M) memory
var bChildIndex = keyIndex(bChildren)
var bKeys = bChildIndex.keys // have "key" prop,object
var bFree = bChildIndex.free //don't have "key" prop,array
// all children of b don't have "key"
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
// all children of a don't have "key"
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
}
}
}
下面,我们来简朴引见下这个排序算法:
- 搜检
a
和b
中的children是不是具有key字段,假如没有,直接返回b
的children数组。 假如存在,初始化一个数组newChildren,遍历
a
的children元素。- 假如aChildren存在key值,则去bChildren中找对应key值,假如bChildren存在则放入新数组中,不存在则放入一个null值。
- 假如aChildren不存在key值,则从bChildren中不存在key值的第一个元素最先取,放入新数组中。
- 遍历bChildren,将一切achildren中没有的key值对应的value或许没有key,而且没有放入新数组的子节点放入新数组中。
- 将bChildren和新数组逐一比较,获得重新数组转换到bChildren数组的
move
操纵patch(即remove
+insert
)。 - 返回新数组和
move
操纵列表。
经由过程上面这个排序算法,我们能够获得一个新的b
的children数组。在运用这个数组来举行比较厚,我们能够将两个children数组之间比较的时候复杂度从o(n^2)转换成o(n)。详细的要领和结果我们能够看下面的DiffChildren算法。
DiffChildren算法
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
}
经由过程上面的重新排序算法整理了今后,两个children比较就只需在雷同下标的状况下比较了,即aChildren的第N个元素和bChildren的第N个元素举行比较。然后较长的谁人元素做insert
操纵(bChildren)或许remove
操纵(aChildren)即可。末了,我们将move操纵再增添到patch中,就能够抵消我们在reorder时对全部数组的操纵。如许只须要一次方便就获得了终究的patch值。
总结
全部Virtual DOM的diff算法设想的异常精致,经由过程三个差别的分部算法来完成了VNode、Props和Children的diff算法,将全部Virtual DOM的的diff操纵分成了三类。同时三个算法又相互递归挪用,对两个Virtual DOM数做了一次(伪)深度优先的递归比较。
下面一片博客,我会引见怎样将获得的VPatch操纵应用到实在的DOM中,从而致使HTML树的变化。