9.4 如何移动元素
移动的是 vnode 对应的真实 DOM,用「新序列中前一个节点的下一个兄弟」当锚点,靠
insertBefore插到它前面。
一、移动的是真实 DOM,不是虚拟节点
- 一个虚拟节点被挂载后,其对应的真实 DOM 保存在
vnode.el中; - 因此可以通过旧子节点的
vnode.el取得它对应的真实 DOM; patchElement中的n2.el = n1.el本质就是 DOM 元素的复用 —— 新节点直接接管旧节点的真实 DOM。
二、确定锚点
要把 newVNode 移动到它在新序列中的前一个节点 prevVNode 的后面:
if (j < lastIndex) {
// newVNode 对应的真实 DOM 需要移动
const prevVNode = newChildren[i - 1]
if (prevVNode) {
// 目标是移到 prevVNode 之后 → 取 prevVNode 真实 DOM 的下一个兄弟节点作为锚点
const anchor = prevVNode.el.nextSibling
// 把 newVNode 的真实 DOM 插到锚点之前,也就是紧跟在 prevVNode 之后
insert(newVNode.el, container, anchor)
}
// prevVNode 不存在 → 当前节点是新序列的第一个,不需要移动
}
prevVNode是新序列中已经就位的节点,newVNode必须紧跟其后;insertBefore(el, anchor)把el插到anchor之前,anchor为null时等价于追加到末尾;- 用
nextSibling而不是prevVNode.el:insertBefore只能表达「插在某个节点之前」。
三、insert 工具函数要支持锚点
insert(el, parent, anchor = null) {
parent.insertBefore(el, anchor)
}
四、推演:old [1,2,3] → new [3,1,2](文本 33 / 11 / 22)
| i | newVNode | j | j < lastIndex | 动作 | DOM 顺序 |
|---|---|---|---|---|---|
| 0 | p3 (33) | 2 | 否 | 更新文本,lastIndex = 2 | p1 p2 p3 |
| 1 | p1 (11) | 0 | 是 | 锚点 = p3 的 nextSibling(null)→ 追加到末尾 | p2 p3 p1 |
| 2 | p2 (22) | 1 | 是 | 锚点 = p1 的 nextSibling(null)→ 追加到末尾 | p3 p1 p2 |
移动是「先摘下、再插到锚点前」;锚点是动态获取的,所以中间过程看起来乱,最终顺序一定正确。
相关
- 9.3 找到需要移动的元素:用 lastIndex 判断哪些节点需要移动
- 9.5 添加新元素:找不到可复用节点时如何挂载新节点