简单来说就是复用已有的DOM,因为DOM频繁卸载挂载会导致性能损耗大,这也是为什么要有Diff算法的原因
key的作用相当于节点的身份证。
// oldChildren
[
{ type: 'p', children: '1', key: 1 },
{ type: 'p', children: '2', key: 2 },
{ type: 'p', children: '3', key: 3 }
]
// newChildren
[
{ type: 'p', children: '3', key: 3 },
{ type: 'p', children: '1', key: 1 },
{ type: 'p', children: '2', key: 2 }
]
上方的vnode中,如果缺少了key, 则完全无法判断应该如何复用。
所以我们认为,当新旧节点的type与key相同时,则可以复用
这两个小章节是一起的,就不再分开了
理解下方代码
function patchChildren(n1, n2, container) {
if (typeof n2.children === '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]
let j = 0 // 这里每次都从0开始递归旧节点,好像有问题。同样的类型不应该复用两次才对啊
for (j; j < oldChildren.length; j++) {
const oldVNode = oldChildren[j]
if (newVNode.key === oldVNode.key) { // 递归遍历,如果key相同则往下走。否则继续循环
patch(oldVNode, newVNode, container) // 复用旧节点,修改内容,以及修改新节点指向的真实DOM
if (j < lastIndex) {
// 代码运行到这里,说明 newVNode 对应的真实 DOM 需要移动
// 先获取 newVNode 的前一个 vnode,即 prevVNode
const prevVNode = newChildren[i - 1]
// 如果 prevVNode 不存在,则说明当前 newVNode 是第一个节点,它不需要移动
if (prevVNode) { // Note: 这个方法其实是前遍历,这个新节点之前的节点应该都是已经排布好了,所以可以直接放到其后面(放到前一个新节点的真实DOM节点的兄弟节点的前面,也就是取代了兄弟节点)
// 由于我们要将 newVNode 对应的真实 DOM 移动到prevVNode 所对应真实 DOM 后面,
// 所以我们需要获取 prevVNode 所对应真实 DOM 的下一个兄弟节点,并将其作为锚点
const anchor = prevVNode.el.nextSibling
// 调用 insert 方法将 newVNode 对应的真实 DOM 插入到锚点元素前面,
// 也就是 prevVNode 对应真实 DOM 的后面
insert(newVNode.el, container, anchor)
}
} else {
lastIndex = j
}
break
}
}
}
} else {
// 省略部分代码
}
}
lastIndex < j
所以lastIndex = j
也就是等于2j < lastIndex
的条件。需要将其移动到前一个新节点的后面,也就是p-3的后面(而此时p-1前的节点都已经是排列好了的)j < lastIndex
的条件,需要将其移动到前一个新节点的后面,也就是p-2的后面。此时遍历完成,真实DOM的顺序与新节点顺序一致。这个排序方法为前循环遍历,当循环到当前节点时,表明当前节点之前的节点已经排好了序列,那么根据顺序,将当前节点排在前一个节点的后边即可。
添加新元素的逻辑也比较简单,就是没有找到type与key相同的元素,则新增一个元素,并将这个元素插到前一个元素的后面即可。这里不做展开。