9.5 添加新元素
新增节点分两步:找到新增节点(旧序列里没有相同 key)→ 挂载到正确位置(先确定锚点再插入)。
一、用 find 标记是否找到可复用节点
外层循环每一轮开始把 find 置为 false,只有在内层循环匹配到相同 key 时才置为 true:
// 遍历新的 children
for (let i = 0; i < newChildren.length; i++) {
const newVNode = newChildren[i]
// 是否在旧的一组子节点中找到可复用节点
let find = false
// 遍历旧的 children
for (let j = 0; j < oldChildren.length; j++) {
const oldVNode = oldChildren[j]
if (newVNode.key === oldVNode.key) {
find = true // 找到可复用节点
patch(oldVNode, newVNode, container)
// ... 移动 / 更新 lastIndex
break
}
}
}
内层循环结束后 find 仍为 false,说明当前 newVNode 在旧子节点中找不到可复用节点,即新增节点。
二、为新增节点确定锚点并挂载
if (!find) {
// 新增节点:先找到正确位置,再挂载
const prevVNode = newChildren[i - 1] // 新序列中的前一个节点
let anchor = null
if (prevVNode) {
// 有前一个节点 → 插到它后面
anchor = prevVNode.el.nextSibling
} else {
// 新序列的第一个节点 → 插到最前面
anchor = container.firstChild
}
// 挂载 newVNode
patch(null, newVNode, container, anchor)
}
anchor = container.firstChild取的是旧的 DOM 列表的第一个元素;此时新节点还没插入,正好用来把新节点插到最前面。prevVNode必然已经就位(要么在前面轮次被更新过,要么是刚挂载的新节点),所以它的nextSibling就是可靠锚点。
三、patch / mountElement 需要接收锚点
挂载的最后一站是 insert(el, container, anchor),因此锚点要从 patchChildren 一路传到 mountElement:
function patch(n1, n2, container, anchor) {
// ...
if (!n1) {
// 挂载时把锚点带下去
mountElement(n2, container, anchor)
} else {
patchElement(n1, n2)
}
// ...
}
function mountElement(vnode, container, anchor) {
// ... 创建元素、设置 children / props
insert(el, container, anchor)
}
patch的签名新增第 4 个参数anchor,并作为mountElement的第 3 个参数向下传递。
四、推演:old [1,2,3] → new [3,1,4,2]
| i | newVNode | 旧序列中能找到? | 动作 |
|---|---|---|---|
| 0 | p3 | 是 | 更新文本 |
| 1 | p1 | 是 | 移动 |
| 2 | p4 | 否 | 新增:锚点 = p1 的 nextSibling,插到 p1 之后 |
| 3 | p2 | 是 | 移动 |
相关
- 9.4 如何移动元素:锚点与 insertBefore 的用法
- 9.6 移除不存在的元素:更新结束后清理多余旧节点