在vue中用于比较新旧vnode的子节点都是一组节点时,为了以最小的性能开销完成更新,需要比较两个子节点,用与比较的算法就叫作diff算法。
看一个例子,通过这组虚拟节点进行更新的步骤(节点顺序相同,且个数相同)
const oldVNode = {
type: 'div',
children: [
{ type: 'p', children: '1' },
{ type: 'p', children: '2' },
{ type: 'p', children: '3' }
]
}
const newVNode = {
type: 'div',
children: [
{ type: 'p', children: '4' },
{ type: 'p', children: '5' },
{ type: 'p', children: '6' }
]
}
function patchChildren(n1, n2) {
if (typeof n2.chilren === 'string') {
// 省略部分代码
} else if (Array.isArray(n2.children)) {
// 更新逻辑
const oldChildren = n1.children
const newChildren = n2.children
for (let i = 0; i < newChildren.length; i++) {
// 更新子节点
patch(oldChildren[i], newChildren[i])
}
} else {
// 省略部分代码
}
}
原理:多余的新节点挂载,多余的旧节点卸载,其他的直接复用
步骤:
代码实现:
function patchChildren(n1, n2) {
if (typeof n2.chilren === 'string') {
// 省略部分代码
} else if (Array.isArray(n2.children)) {
// 更新逻辑
const oldChildren = n1.children
const newChildren = n2.children
// 旧节点的长度
const oldlen = oldChildren.length
// 新节点的长度
const newlen = newChildren.length
const commonLength = Math.min(oldlen, newlen)
for (let i = 0; i < commonLength; i++) {
// 更新节点
patch(oldChildren[i], newChildren[i])
}
// newlen > oldlen, 需要挂载
if (newlen > oldlen) {
for (let i = commonLength; i < newlen; i++) {
patch(null, newChildren[i])
}
} else if (oldlen > newlen) {
// oldlen > newlen, 需要卸载
for (let i = commonLength; i < oldlen; i++) {
unmount(null, oldChildren[i])
}
}
} else {
// 省略部分代码
}
}
分为两种情况:
比较方法:通过key值进行比较
// type不同的虚拟节点
// oldVNode
[
{ type: 'p' },
{ type: 'div' },
{ type: 'span' },
]
// newChildren
[
{ type: 'span' },
{ type: 'p' },
{ type: 'div' },
]
// type相同数据不同的虚拟节点
// oldVNode
[
{ type: 'p', children: '1' },
{ type: 'p', children: '2' },
{ type: 'p', children: '3' }
]
// newChildren
[
{ type: 'p', children: '3' },
{ type: 'p', children: '2' },
{ type: 'p', children: '1' }
]
第一步:取新的一组子节点中的第一个节点p-3,它的key为3。尝试在旧的一组子节点中找到具有相同key值的可复用节点,如果能找到,记录旧子节点中当前节点的索引为2
第二步:取新的一组子节点中的第二个节点p-1,它的key为1。尝试在旧的一组子节点中找到具有相同key值的可复用节点,如果能找到,记录旧子节点中当前节点的索引为0
此时索引值的递增顺序被打破。节点p-1在旧children中的索引是0,它小于节点p-3在旧children中的索引2。说明p-1在旧children中排在节点p-3前面,但在新的children中,他排在节点p-3后面。 所以,我们得到结论:节点p-1对应的真实DOM需要移动。
第三步:取新的一组子节点中的第二个节点p-2,它的key为2。尝试在旧的一组子节点中找到具有相同key值的可复用节点,如果能找到,记录旧子节点中当前节点的索引为1
同理,节点p-2在旧children中排在节点p-3前面,但在新的children中,它排在节点p-3后面。因此,节点p-2对应的真实DOM也需要移动
此时索引值的递增顺序被打破。节点p-1在旧children中的索引是0,它小于节点p-3在旧children中的索引2。说明p-1在旧children中排在节点p-3前面,但在新的children中,他排在节点p-3后面。 所以,我们得到结论:节点p-1对应的真实DOM需要移动。
关键点:
关键点:找最大的索引值,索引值小于最大的索引值,表示需要移动真实DOM
把新节点一个一个得去旧节点中找
lastIndex:用来存储寻找过程中当前节点在旧的Children中最大的索引值。比如第一次 i 的循环找到的最大索引为2,但是第二轮找到的节点为0,那么0<2就证明是需要移动。(为什么?因为新节点按照 i 的循环,第二次循环的节点是递增的,但是结果0<2说明旧节点的不是递增,证明新节点和旧节点的顺序不一样,就需要更新)
function patchChildren(n1, n2) {
if (typeof n2.chilren === 'string') {
// 省略部分代码
} else if (Array.isArray(n2.children)) {
// 更新逻辑
const oldChildren = n1.children
const newChildren = n2.children
// 用来存储寻找过程中当前节点在旧的Children中最大的索引值
let lastIndex = 0
for (let i = 0; i < newChildren.length; i++) {
const newVNode = newChildren[i]
for (let j = 0; j < oldChildren.length; j++) {
const oldVNode = oldChildren[i]
if (newVNode.key === oldVNode.key) {
patch(oldVNode, newVNode)
if (j < lastIndex) {
// 如果当前找到的节点在旧Children中的索引小于lastIndex
// 说明我们的当前节点对应的真实dom是需要移动的
} else {
lastIndex = j
}
}
break;
}
}
} else {
// 省略部分代码
}
}
第一步:首先将旧子节点上的真实dom赋值给对应新节点的dom(可复用的旧节点,为什么要将旧子节点的真实DOM赋值给新节点的DOM?因为最终操作DOM是通过新节点的真实DOM去操作)
第二步:找到需要移动的虚拟节点,将它上一个虚拟节点对应的真实DOM的下一个兄弟节点作为锚点
第三步:将当前虚拟节点对应的真实dom移动到锚点位置
(这里是通过找到需要移动的虚拟节点newVnode的上一个节点的位置,如果上一个节点不存在,表示已经是第一个节点不需要移动,如果存在则将该节点的真实DOM移动到上一个节点的下一个兄弟节点的位置)
vnode里面的el存储的是真实的DOM节点,移动的时候就是将旧的虚拟节点的el属性赋值给新的虚拟节点的el;最终是通过新子节点的虚拟节点的顺序来操作真实DOM。
问题?如何移动真实DOM这里移动的真实DOM指的是旧的虚拟DOM对应真实DOM?然后移动完成以后将旧的真实DOM再赋值给对应新节点的DOM?
回答:diff算法中对比出新旧节点位置不一致后,要么移动新节点的真实DOM,要么移动旧节点的真实DOM去保证新旧节点的DOM的顺序保持一致的,而vue底层是移动旧节点的真实DOM的顺序
真实DOM移动完成后不会将旧节点的真实DOM赋值给新节点。diff算法本身存在就是为了对比新旧节点是否相同,如果节点相同只需要复用节点DOM更改节点里面的内容即可,从而保证效率。
function patchChildren(n1, n2) {
if (typeof n2.chilren === 'string') {
// 省略部分代码
} else if (Array.isArray(n2.children)) {
// 更新逻辑
const oldChildren = n1.children
const newChildren = n2.children
// 用来存储寻找过程中最大的索引值
let lastIndex = 0
for (let i = 0; i < newChildren.length; i++) {
const newVNode = newChildren[i]
for (let j = 0; j < oldChildren.length; j++) {
const oldVNode = oldChildren[i]
if (newVNode.key === oldVNode.key) {
patch(oldVNode, newVNode)
if (j < lastIndex) {
// 如果当前找到的节点在旧Children中的索引小于lastIndex
// 说明我们的当前节点对应的真实dom是需要移动的
// 先获取当前newVNode的上一个节点
const prevVNode = newChildren[i - 1]
if (prevVNode) {
// 如果prevVNode不存在,说明prevVNode就是第一个,不需要移动
const anchor = prevVNode.el.nextSibling
insert(newVNode.el, anchor) //将DOM插入到锚点位置
//vnode里面的el存储的是真实的DOM节点,移动的时候就是将旧的虚拟节点的el属性赋值给新的虚拟节点的el;最终是通过新子节点的虚拟节点的顺序来操作真实DOM。
}
} else {
lastIndex = j
}
}
break;
}
}
} else {
// 省略部分代码
}
}
Document