一句话总结
Vue2 用 Object.defineProperty 劫持对象的每个属性——必须递归初始化,监听不到新增/删除属性和数组下标,靠重写 7 个数组方法和 $set 打补丁;Vue3 用 Proxy 代理整个对象,增删改、下标、length 全都能感知,且嵌套对象访问时才代理(懒代理),性能更好。
初级理解
Vue2 的三大限制:对象新增属性、数组按索引赋值、修改数组 length 都不是响应式的,官方提供 Vue.set / this.$set、Vue.delete 和重写后的 7 个数组方法(push/pop/shift/unshift/splice/sort/reverse)来解决。
Vue3 的两个响应式 API:reactive 包装对象(基于 Proxy),ref 包装任意值(基本类型必须用它),通过 .value 访问。
中级深入
依赖收集与派发更新(Vue2):每个响应式属性对应一个 Dep;渲染时 Watcher 读取属性触发 getter,把当前 Watcher 收集进 Dep(依赖收集);数据变化触发 setter,通知 Dep 里所有 Watcher 重新渲染(派发更新)。数组因为没有 setter,靠拦截原型方法手动 notify。
Vue3 的 track/trigger:用全局 activeEffect 替代 Dep.target,用 targetMap → key → Set<effect> 三级结构存依赖。Proxy 的 handler 里 get 时 track、set 时 trigger,逻辑集中且不用递归。
ref 为什么需要 .value:基本类型(数字、字符串)无法被 Proxy 代理(Proxy 只能代理对象),所以 ref 把值包成 { value } 对象再代理;同时 ref 也支持对象类型(内部自动调用 reactive)。
高级拓展
Vue3 懒代理(惰性响应式):Vue2 初始化时必须递归遍历所有嵌套属性全部劫持,大对象初始化开销大;Vue3 只代理最外层,嵌套对象在 get 访问到时才递归创建 Proxy,按需开销。
effect 与调度器:effect 支持自定义 scheduler——渲染 effect 的 scheduler 把更新任务推入去重的微任务队列(同一 tick 内多次修改只渲染一次),这就是 nextTick 批量更新的底层。watch 本质是“effect + scheduler 回调”。
其他进阶 API:shallowReactive/shallowRef(只代理第一层,适合大表格/图表实例)、markRaw(永远不代理,如 Three.js 对象)、toRaw(拿回原始对象)、readonly(只读代理)。
实战场景
场景一:解构 reactive 丢失响应式
场景二:Vue3 reactive 整体替换不生效
场景三:大表格渲染慢
面试模拟
Q:为什么 Vue2 无法监听数组下标修改?Vue3 为什么可以?
A:Vue2 用 defineProperty 劫持的是属性,数组是按索引存取的稀疏集合,给上千个索引都装 getter/setter 性能不可接受,官方权衡后只重写了会改变原数组的 7 个方法。Vue3 的 Proxy 代理的是整个对象,任何 get/set/deleteProperty(包括索引和 length)都会进入 handler,天然覆盖。
Q:ref 和 reactive 怎么选?
A:简单规则——基本类型必须 ref;对象/数组两者皆可。工程实践上推荐统一用 ref(.value 心智一致、可整体替换、不会踩解构丢失响应式的坑),reactive 适合聚合一组强相关的表单/配置字段。ref 存对象时内部会自动调 reactive。
Q:Vue3 的依赖收集为什么不用递归初始化就能工作?
A:Proxy 的 get 是运行时拦截——嵌套对象只有在被访问时才触发 get,此时再对返回值创建代理并收集依赖(懒代理)。初始化只需代理最外层,复杂度从"启动时全量"变成"访问时增量",这也是 Vue3 初始化性能提升的原因之一。