虚拟DOM和真实DOM
真实DOM是以各种网页元素为节点组成的一颗树,而虚拟DOM就是模拟树形结构的数组。虚拟DOM相对于真实DOM操作简单、快捷,所以在构造真实DOM前,会将需要的操作在虚拟DOM上完成,再使用虚拟DOM构造真实DOM。
比如以下网页代码
<ul id="list">
<li class="item">哈哈</li>
<li class="item">呵呵</li>
<li class="item">嘿嘿</li>
</ul>
对应的真实DOM为(节点上的属性如id、class等省略)
对应的虚拟DOM为:
let VDOM = {
tagName: 'ul', // 标签名
props: { // 标签属性
id: 'list'
},
children: [ // 标签子节点
{
tagName: 'li', props: { class: 'item' }, children: ['哈哈']
},
{
tagName: 'li', props: { class: 'item' }, children: ['呵呵']
},
{
tagName: 'li', props: { class: 'item' }, children: ['嘿嘿']
},
]
}
这时候,如果修改修改一个li标签的文本:
<ul id="list">
<li class="item">哈哈</li>
<li class="item">呵呵</li>
<li class="item">林三心哈哈哈哈哈</li> // 修改
</ul>
真实DOM的对应节点的文本内容会改变,生成的新虚拟DOM为:
let newVDOM = { // 新虚拟DOM
tagName: 'ul', // 标签名
props: { // 标签属性
id: 'list'
},
children: [ // 标签子节点
{
tagName: 'li', props: { class: 'item' }, children: ['哈哈']
},
{
tagName: 'li', props: { class: 'item' }, children: ['呵呵']
},
{
tagName: 'li', props: { class: 'item' }, children: ['林三心哈哈哈哈哈']
},
]
}
虚拟DOM为什么可以提高性能
1、虚拟DOM是对真实DOM的抽象,去除了很多DOM节点的属性,提高了比较的效率
2、diff算法需要虚拟DOM的配合,每个操作都去更新虚拟DOM而不是把每个操作应用到真实DOM上触发多次回流和重绘
Vue中的diff算法
背景
如果每次都用新的虚拟DOM去更新真实DOM时,那么整颗树都要重新渲染,所以产生了diff算法,需要新的虚拟DOM和旧的虚拟DOM,找到新旧虚拟DOM中不同的位置,只对不同位置进行真实DOM的更新。
比较策略
先贴一下源码
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0
let 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, idxInOld, vnodeToMove, refElm
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
//定义映射数组
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
//在映射数组中查找新节点对应的旧节点
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
//没有相同的key,没有对应的旧节点,为新节点
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else { //有相同的key
vnodeToMove = oldCh[idxInOld] //找到该key对应的旧节点
//再一次判断相同key的新旧节点是否为完全相同
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
diff算法只在DOM树的同层级进行比较,不会跨层比较,所以可以把同层的数据看成处于一个数组中,新虚拟DOM为newCh,旧虚拟DOM为oldCh,对应源码。
该函数定义了4个指针,分别指向新旧DOM的首尾。
可以看到循环中分7种情况:
前两种情况是对未定义节点的处理,此处忽略。
后5种情况:
1、旧虚拟DOM首节点和新虚拟DOM首节点使用sameVnode方法(判断节点类型是否相同)进行比较,如果判断是相同节点,那么直接复用,更新到新节点相应位置,原位置节点置为undefined。
2、旧虚拟DOM尾节点和新虚拟DOM尾节点使用sameVnode方法进行比较,处理与1相同
3、旧虚拟DOM首节点和新虚拟DOM尾节点使用sameVnode方法进行比较,处理与1相同
4、旧虚拟DOM尾节点和新虚拟DOM首节点使用sameVnode方法进行比较,处理与1相同
5、如果以上逻辑都匹配不到,再把所有旧子节点的 key 做一个映射到旧节点下标的 key -> index 表,然后遍历该表找出该新节点在旧节点中有无可复用的节点。(遍历没有被判断过的旧节点,首先找到key相同的节点,若没有,则直接创建新节点;若有,使用sameNode方法判断两个节点是否相同,如果是,则复用,再把该节点移动到新节点的位置,原位置节点置为undefined;如果不是,就当作新节点,创建节点,插入到指定位置)
如果有一个数组已经遍历完就跳出循环,此时已经完成较短数组的所有处理,剩下未处理的是较长数组中的节点。如果是旧数组较长,那么需要删除剩余节点;如果是新数组较长,那么需要新增剩余节点。
key
说到key,就要讲一下sameVnode函数,源码:
function sameVnode (a, b) {
return (
a.key === b.key && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}
可以看到key出现在a.key === b.key中,并且位于判断条件的第一项。这就意味着,我们为节点设置key的目的是标记那些完全一样的节点,帮助diff操作更加准确和快速。
无key
如果没有key,那么a.key和b.key都为undefined,则条件成立。并且由于后面的判断条件较松,所以很多不应该被拿来复用的节点被判断成sameVnode进行patch,这就导致了就地复用(直接将新老虚拟DOM中对应位置的节点进行复用),需要进行大量的操作来使两个节点相同,并且原本可以复用的节点也因为此原因需要拆掉重建,从而消耗性能。
有key
如果有key,那么会根据唯一的key来判断节点是否为相同的,当两个节点key相同时,意味着新老节点没有差异,可以完全复用,此时的操作就比较简单,并且会将老虚拟DOM中所有可复用的节点全部复用,从而提升性能。
其实有key也可以用key来生成map,直接用key来查找,比遍历更加快速
例
old:a b c d
new: b c d a
有key情况和无key的情况
根据diff算法的比较策略
无key:从前往后每个节点都会被判断成相同,所以从前往后依次patch内容
有key:根据key来找可复用的节点,所以顺序是
先把a移动到最后
从b开始依次从前往后复用
react和vue的diff区别
-
vue比对节点,当节点元素类型相同,但是className不同,认为是不同类型元素,删除重建,而react会认为是同类型节点,只是修改节点属性
-
vue的列表比对,采用从两端到中间的比对方式,而react则采用从左到右依次比对的方式。当一个集合,只是把最后一个节点移动到了第一个,react会把前面的节点依次移动,而vue只会把最后一个节点移动到第一个。总体上,vue的对比方式更高效。