move操作是diff算法识别出节点位置变化但可复用时,直接迁移真实dom节点到新位置的高效更新方式;它避免remove+mount带来的事件丢失、状态重置、重排重绘及js层开销,前提是新旧vnode类型一致、key匹配且处于乱序区,由双端查找或map映射精准定位后通过insertbefore等api执行物理移动。

渲染器中的 move 操作,是指在 Diff 算法识别出节点“位置变化但无需重建”后,对真实 DOM 节点执行的物理迁移——即不销毁、不重新创建,而是直接调用 insertBefore 或 appendChild 将已有节点插入到新位置。它不是虚拟节点的逻辑重排,而是浏览器中真实元素的位移动作。
为什么 move 比 remove + mount 更高效?
DOM 节点一旦被移除(removeChild),其绑定的事件监听器、内部状态(如 <input> 的光标位置、滚动偏移、CSS 动画进度)、以及关联的 layout/paint 缓存都会丢失。而 move 保留全部上下文,仅改变父容器内的兄弟顺序:
- 避免重新触发 layout(重排)和 paint(重绘)的完整流程
- 保持
focus、selection、scrollTop等用户交互状态 - 跳过 vnode 创建、props 解析、组件实例挂载等 JS 层开销
Diff 算法如何识别出该 move?
move 不是凭空发生的,它依赖于 Diff 过程中对可复用节点的精准定位。关键条件有三个:
- 类型一致:新旧 vnode 的 tag、key、isComponent 标识完全匹配
- 位置错位:该节点在新旧子节点数组中的索引不同(比如旧序列为 [A,B,C],新序列为 [C,A,B])
- 未被前置/后置预处理覆盖:它既不在开头连续相同段,也不在结尾连续相同段,落入中间“乱序区”
此时算法会进入“双端查找”或“map 索引映射”阶段。例如 Vue 3 中,当发现新节点 C 的 key 在旧节点 map 中存在,且其旧位置索引 oldIndex ≠ 当前应插入位置 newIndex,就判定需 move,并记录 move(nodeC, anchor) 操作。
move 的实际执行逻辑(以浏览器 API 为例)
最终渲染器将 move 指令转为原生 DOM 调用。核心是确定 锚点(anchor) —— 即目标插入位置的前一个兄弟节点:
- 若插入到列表第 2 位,则 anchor 是当前第 1 位的节点
- 若插入到开头,则 anchor 是原父容器的
firstChild - 若插入到末尾,则调用
appendChild,等价于 anchor 为null
例如:parentNode.insertBefore(nodeC, anchorNode) —— 这一行就完成了 move,浏览器内部只是调整指针,不触及 nodeC 的属性树或样式计算。
哪些情况看似 move,实则 fallback 到 mount?
并非所有视觉上的“移动”都会触发 move。以下情形会退化为卸载旧节点 + 挂载新节点:
- 节点无
key,仅靠 tag 和 children 推断,但结构不满足复用条件(如 props 变化导致强制更新) - 跨父级移动(如从
div#list1移入div#list2),Diff 默认按层级进行,不跨父查找 - 组件节点发生
type变更(如Foo→Bar),即使 key 相同也视为不可复用
这类 fallback 虽然语义上仍是“位置变化”,但因无法安全复用状态,move 就让位于正确性优先的 mount/unmount 流程。










