尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Vue 中 DOM Patch 失败:从根源到实践

Vue 中 DOM Patch 失败:从根源到实践 Vue 中 DOM Patch 失败从根源到实践一次insertBefore报错引发的思考——为什么你手动改 DOMVue 会发疯。一、问题的本质Vue 内部维护着一棵虚拟 DOM 树vnode它记录着每个节点的结构、属性以及它在真实 DOM 中的位置引用。Vue 每次更新视图时都是拿着 vnode 里的这些引用去操作真实 DOM。当你在 Vue 不知情的情况下手动改变了真实 DOM 的结构vnode 里的引用就会失效。下次 Vue 执行 patch 时拿着旧引用去找目标节点发现它已经不在原来的位置了于是抛出textNotFoundError: Failed to execute insertBefore on Node: The node before which the new node is to be inserted is not a child of this node.一句话Vue 的账本vnode和现实真实 DOM对不上。二、最简单的复现例子vuetemplate div refbox p v-ifshowAA/p pB/p /div /template script export default { data() { return { showA: true } }, mounted() { // 手动把 A 删了但没改 showA const box this.$refs.box box.removeChild(box.children[0]) }, methods: { toggle() { this.showA false // 触发 Vue 更新 } } } /script发生了什么初始渲染后DOM 是pA/ppB/pVue 的 vnode 里记录着 A 和 B 两个节点以及 A 的引用。mounted里手动removeChild删掉了 A。真实 DOM 只剩pB/p但Vue 完全不知道vnode 里 A 还在而且 Vue 认为 A 还在 B 前面。调用toggle()showA变成falseVue 准备把 A 移除。它拿着 vnode 里 A 的引用去操作真实 DOM发现 A 已经不在父节点里了于是报错。为什么不同步Vue 的 vnode 说A 还在真实 DOM 说A 没了。两边对不上。三、哪些操作会导致失去同步判断标准只有一个这个操作是否改变了 DOM 的结构而 Vue 不知情。结构 节点的增、删、移动、父子关系变化。❌ 会出问题的操作操作代码示例为什么出问题删除节点el.parentNode.removeChild(el)vnode 里还记着这个节点引用失效移动节点otherParent.appendChild(el)Vue 以为节点还在原位实际换了地方替换节点parent.replaceChild(newEl, oldEl)vnode 存的是 oldEl 的引用已失效重写父节点内容container.innerHTML xxxVue 管理的所有子节点被一次性抹掉插入新节点parent.insertBefore(newEl, vueEl)Vue 不知道多了兄弟节点索引错位改变父子层级grandParent.appendChild(parent)Vue 记录的父子/兄弟关系全变了✅ 不会出问题的操作操作代码示例为什么安全改样式el.style.display none节点还在原位结构没变改类名el.classList.add(active)只改属性不影响位置和身份改属性el.setAttribute(title, xxx)只改节点自身属性改文本el.textContent xxx可能被 Vue 覆盖但不报错操作 Vue 范围外document.body.appendChild(div)vnode 树里根本没有这些节点操作 ref 空容器内部this.$refs.chart.innerHTML ...Vue 只挂载容器本身不关心内部记忆口诀动了谁在哪儿、谁挨着谁就会出问题只动长什么样就没事。四、如何避免正确操作 DOM 的原则核心原则在 Vue 管辖范围内改样子可以改结构必须交给 Vue。1. 优先用数据驱动不碰 DOM你想做的事错误做法正确做法隐藏/显示元素el.style.display nonev-if/v-show 数据增删列表项parent.removeChild(el)改数组v-for自动更新切换 classel.classList.add(active):class{ active: isActive }改文本el.textContent xxx{{ text }}改属性el.setAttribute(disabled, ):disabledisDisabled2. 确实需要操作 DOM 时遵守三条边界只改属性/样式不改结构el.style.color red、el.focus()、el.scrollIntoView()都安全。要改结构就改数据让 Vue 去改 DOM用this.list.splice()而不是removeChild。操作 Vue 管不着的区域给第三方库一个独立的空容器让它在内部折腾。3. 自定义指令的正确写法js// ❌ 危险直接删节点 Vue.directive(permission, { inserted(el, binding) { if (!hasPermission(binding.value)) { el.parentNode.removeChild(el) // Vue 不知道vnode 失效 } } }) // ✅ 安全只隐藏不删除 Vue.directive(permission, { inserted(el, binding) { if (!hasPermission(binding.value)) { el.style.display none // 节点还在Vue 引用有效 } } })4. 必须等待 DOM 更新时用$nextTickjsthis.showPanel true this.$nextTick(() { // 此时 DOM 已经更新完毕可以安全地读取/操作 this.$refs.panel.scrollIntoView() })五、决策图text你想操作 DOM │ ├─ 能不能用数据表达 │ │ │ ├─ 能 → 改数据让 Vue 去操作 DOM ✅ │ │ │ └─ 不能 → 继续往下 │ ├─ 是改结构增/删/移动吗 │ │ │ ├─ 是 → 改数据让 Vue 去改 ✅ │ │ │ └─ 不是 → 继续往下 │ ├─ 这个节点在 Vue 管辖范围内吗 │ │ │ ├─ 在 → 只改属性/样式不改结构 ✅ │ │ │ └─ 不在 → 随便操作 ✅ │ └─ 需要等 DOM 更新完再操作 │ └─ 用 $nextTick 包起来 ✅六、数据驱动视图Vue 的核心思想命令式 vs 声明式命令式传统 DOM 操作你亲自指挥每一步jsconst ul document.getElementById(list) data.forEach(item { const li document.createElement(li) li.textContent item.name ul.appendChild(li) })你关心的是过程创建什么、插到哪里、删掉哪个。DOM 结构由你手动维护一旦数据变了你得自己记得同步 DOM。声明式数据驱动你只描述结果Vue 负责实现vueul li v-foritem in list :keyitem.id{{ item.name }}/li /ul你关心的是结果列表里有什么。DOM 怎么变、哪个节点复用、哪个节点新建全部由 Vue 的虚拟 DOM diff 算法决定。数据驱动的工作流程你改数据this.list.push(...)Vue 侦测到变化通过Object.definePropertyVue 2或ProxyVue 3拦截。触发重新渲染Vue 生成新的虚拟 DOM 树。diff 对比新旧两棵虚拟树对比算出最小变更。patch 真实 DOMVue 拿着自己的 vnode 引用去操作真实 DOM因为 vnode 和真实 DOM 始终由 Vue 自己同步维护所以永远不会出现锚点找不到的问题。关键在第 5 步DOM 是 Vue 自己改的它当然知道 DOM 长什么样。你手动改 DOM 之所以出问题就是因为 Vue 不知道你改了。七、最终结论你手动改 DOM数据驱动谁改 DOM你VueVue 知道吗不知道知道vnode 和真实 DOM可能不一致始终一致下次 patch可能报错永远安全核心规则结构交给 Vue样子自己改范围外的随便动更新后用 nextTick。只要你的代码没有在 Vue 不知情的情况下增删移动它管理的节点就永远不会出现 DOM patch 问题。本文基于一次真实的insertBefore报错排查整理而成。核心思路是理解 Vue 的虚拟 DOM 与真实 DOM 之间的账本关系守住 DOM 所有权边界。
返回列表