要成为真正的 Vue 高手,不能停留在"会用"的层面。理解响应式系统和 Diff 算法的底层实现,不仅能帮你写出更高效的代码,更能在遇到诡异的 bug 时快速定位根因。本文将深入 Vue 3 源码,剖析响应式原理和 Diff 算法的核心实现。
Vue 2 的 Object.defineProperty 困境
Vue 2 的响应式基于 Object.defineProperty,存在三个致命缺陷:无法检测属性的添加和删除(必须使用 Vue.set);数组变更无法直接检测(索引修改和 length 修改不触发更新);性能问题——大对象初始化时,defineProperty 的递归遍历开销很大。
Vue 3 的 Proxy:真正的响应式
Vue 3 使用 ES6 的 Proxy 彻底解决了这些问题。Proxy 可以拦截 13 种操作,包括属性的增、删、改、查。核心实现:
function reactive(target) {\n if (!isObject(target)) return target;\n return new Proxy(target, {\n get(target, key, receiver) {\n track(target, key); // 依赖收集\n const result = Reflect.get(target, key, receiver);\n if (isObject(result)) return reactive(result); // 懒代理\n return result;\n },\n set(target, key, value, receiver) {\n const oldValue = target[key];\n const result = Reflect.set(target, key, value, receiver);\n if (oldValue !== value) trigger(target, key); // 触发更新\n return result;\n },\n deleteProperty(target, key) {\n const hadKey = Object.hasOwn(target, key);\n const result = Reflect.deleteProperty(target, key);\n if (hadKey && result) trigger(target, key);\n return result;\n },\n });\n}Proxy 的核心优势:拦截所有操作(13 种拦截器)、懒代理(嵌套对象只在被访问时才变成响应式)、数组支持(直接支持数组的索引修改和 length 修改)。
依赖收集与触发更新的核心:track 和 trigger
Vue 3 使用一个全局的 WeakMap 结构来管理依赖:targetMap: WeakMap→Map→Set。每个响应式对象的每个 key 都维护了一个副作用函数的集合,当 key 变化时,所有依赖于该 key 的副作用函数都会重新执行。
const targetMap = new WeakMap();\nlet activeEffect = null;\n\nfunction track(target, key) {\n if (!activeEffect) return;\n let depsMap = targetMap.get(target);\n if (!depsMap) targetMap.set(target, (depsMap = new Map()));\n let deps = depsMap.get(key);\n if (!deps) depsMap.set(key, (deps = new Set()));\n deps.add(activeEffect);\n}\n\nfunction trigger(target, key) {\n const depsMap = targetMap.get(target);\n if (!depsMap) return;\n const effects = depsMap.get(key);\n if (effects) [...effects].forEach(effect => effect());\n}虚拟 DOM 与 Diff 算法
每次状态变化时重新渲染整个 DOM 树是极其低效的。Vue 的解决方案是:用虚拟 DOM(轻量级的 JavaScript 对象)描述 UI,当状态变化时生成新的虚拟 DOM 树,通过 Diff 算法找到新旧两棵树之间最小的差异,然后只更新变化的部分。
Vue 3 的快速 Diff:双端对比 + 最长递增子序列
Vue 3 的 Diff 算法分为五个步骤,复杂度优化到接近 O(n):
头头对比:从头部开始对比,相同则跳过,直到遇到不同的节点。
尾尾对比:从尾部开始对比,相同则跳过。
新增节点:当旧序列处理完了,新序列还有剩余时,剩余的都需要挂载。
删除节点:当新序列处理完了,旧序列还有剩余时,剩余的都需要卸载。
未知序列处理(核心)——最长递增子序列:这是 Vue 3 Diff 最精妙的部分。通过 key 建立新节点在旧节点中的位置映射,找出最长递增子序列(这些节点不需要移动),其余节点进行移动或挂载操作。通过最大化"不动"的节点数量,最小化 DOM 操作。
key 的重要性
没有 key 时,Vue 使用"就地复用"策略——只更新内容,不移动 DOM。这在大多数场景下是可以接受的,但当列表项有内部状态(如输入框内容、动画状态)时,会导致状态错位。而使用 index 作为 key 值在某些场景下和没有 key 效果一样差。
总结
Vue 3 的响应式系统用 Proxy 实现了真正的"全量拦截",而 Diff 算法通过双端对比 + 最长递增子序列将 DOM 更新优化到了接近理论最优。深入理解这两个核心机制,你就能在编写 Vue 代码时做出更精准的性能决策。
评论 (0)