Virtual DOM
为什么要使用Virtual DOM?
- 将得到的变更通知生成新的Virtual DOM树。将新的和旧的进行diff patch操作,减少了直接通过DOM API去增删改查DOM的操作,提高开发效率。
-
All problem in computer science can be resolved by another level of indirection
软件开发中的所有问题都可以通过增加一层抽象来解决。(关注点分离)
Virtual DOM是分层思想的一种体现
- 框架将DOM抽象成Virtual DOM后可以应用在各个终端
Diff策略
1、 按tree层级diff(level by level)
- 在Web UI中很少会出现DOM层级会因为交互而产生更新
- 在新旧节点之间按层级进行diff
2、 按类型进行diff
- 不同类型的节点之间往往差异很大,为了提升效率,只会对相同类型节点进行diff
- 不同类型会直接创建新类型节点,替换旧类型节点
- 下图中,由上一层图形变为下一层图形。同层比较,第二列五角星和三角形不同,虽然子节点的两个五角星相同,但是也会直接将三个五角星直接销毁,替换为新的节点。
3、 列表Diff
- 给列表元素设置key,可以提升效率
Diff过程
- 参考文章:详解Vue的Diff算法
updateChildren(parentElm, oldCh, newCh) {
let oldStartIdx = 0, newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx
let idxInOld
let elmToMove
let before
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) { // 对于vnode.key的比较,会把oldVnode = null
oldStartVnode = oldCh[++oldStartIdx]
} else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx]
} else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode)
api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode)
api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 使用key时的比较
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 有key生成index表
}
idxInOld = oldKeyToIdx[newStartVnode.key]
if (!idxInOld) {
api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
newStartVnode = newCh[++newStartIdx]
}
else {
elmToMove = oldCh[idxInOld]
if (elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
} else {
patchVnode(elmToMove, newStartVnode)
oldCh[idxInOld] = null
api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el)
}
newStartVnode = newCh[++newStartIdx]
}
}
}
if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
} else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
-
updateChildren
代码分析
- 循环成立条件:OldStart小于等于OldEnd && NewStart小于等于NewEnd时
- 判断VNode是否为null,是指针指向下一个节点
-
不是则按照 OS和NS(S|)、OE和NE(E|)、 OS和NE(\ )、OE和NS(/)的顺序进行比较判断
若相同S| 节点位置不变,指针+1 E| 节点位置不变,指针-1 \ OldStart移动到OldEnd之后,OldStart +1, NewEnd -1 / OldEnd移动到OldStart之前,NewStart +1, OldEnd -1
若不同,根据key生成OldStart和OldEnd之间的index表,查找元素是否在OldStart和OldEnd之间
若有,直接移动到OldStart前 若没有,创建后,移动到OldStart前
- 判断如果NewStart > NewEnd,说明新节点已经遍历完,删除OldStart和OldEnd之间的DOM
如果OldStart > OldEnd,说明旧节点已经遍历完,将多的新节点根据index添加到DOM中去
- 例:如图,灰色表示Virtual DOM 深色表示真实的DOM
-
四个指针
- OldStartIdx 旧开始节点
- OldEndIdx和 旧结束节点
- NewStartIdx 新开始节点
- NewEndIdx 新结束节点
- 判断OldStartIdx和NewStartIdx是否相同。1和1相同,两个Start指针都往右移动一位(+1)
- 继续比较OldStartIdx和NewStartIdx是否相同。2和5不同,改为比较OldEndIdx和NewEndIdx
- 6和6相同,两个End指针都往左移动一位(-1)。5和2不一致,改为比较OldStartIdx和NewStartIdx
- 2和5不同,改为比较OldEndIdx和NewEndIdx,也不同。改为比较OldStartIdx和NewEndIdx(方向)。2和2相同,将OldStartIdx对应的真实DOM移动到OldEndIdx之后,同时OldStartIdx右移一位,NewEndIdx左移一位。
- 继续比较Start,不同,比较End,也不同。比较OldStart和NewEnd,不同。比较OldEnd和NewStart(/方向),相同。移动OldEnd对应的真实DOM到OldStart之前。同时OldEnd左移,NewStart右移。
- 继续循环,Start| End| / 四个方向,都不同。根据key生成OldStart和OldEnd之间的index表,查找7是否在OldStart和OldEnd之间。如果找到,直接挪到OldStart之前。找不到,则说明是新节点,将7由VirtualDOM生成真实DOM后挪到OldStart之前。
- NewEnd左移,此时NewEnd小于NewStart,结束循环
- 删除OldStart和OldEnd之间的部分
- 新Virtual DOM已存在,DOM节点也已生成,销毁旧Virtual DOM列表
- 设置key之后,就不要遍历了。算法复杂度为O(n),否则最坏情况为$O(n^2)$
- vue2+中并没有完整的patch过程,节点操作是在diff操作过程中同时进行的,提升了增删改查DOM节点时的效率
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。