Vue3 响应式原理是什么?
简化版
Vue3 响应式基于 Proxy 劫持对象读写。读取属性时收集依赖,修改属性时触发依赖更新。核心流程是 reactive 创建代理对象,effect 执行时通过 track 收集依赖,数据变化时通过 trigger 通知副作用重新执行。
详细版
简化模型:
const bucket = new WeakMap();
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key);
return target[key];
},
set(target, key, value) {
target[key] = value;
trigger(target, key);
return true;
},
});
}
依赖关系大致是:
target -> key -> effects
Vue2 用 Object.defineProperty,对新增属性、数组索引等处理不够自然;Vue3 用 Proxy,可以更完整地拦截对象操作。
完整版教学
一、响应式解决什么问题
响应式的目标是:数据变了,使用这份数据的视图或计算逻辑自动更新。开发者不需要手动找 DOM 再改文本。
Vue 会在组件渲染时读取响应式数据,这个读取过程就是收集依赖的机会。之后数据变化,Vue 就知道哪些组件或 effect 需要更新。
二、依赖收集发生在 getter
当执行副作用函数时:
effect(() => {
document.body.textContent = state.count;
});
读取 state.count 会触发 Proxy 的 get,Vue 把当前 effect 记录到 state/count 对应的依赖集合里。
依赖不是“整个 state 对应整个页面”,而是按原始目标和属性 key 建立映射。只有 effect 执行期间实际读取到的属性才会订阅,因此 state.name 的变化不应无条件触发只读取 state.count 的 effect。
三、派发更新发生在 setter
当执行:
state.count++;
Proxy 的 set 会触发,Vue 找到之前收集的依赖集合,把相关 effect 加入调度队列。真实 Vue 不会每次都立刻同步更新 DOM,而是通过 scheduler 批量异步更新,避免一次事件里多次修改造成重复渲染。
四、ref 和 reactive 的区别
reactive 主要代理对象,ref 用来包装基本类型或单个值:
const count = ref(0);
count.value++;
模板里 ref 会自动解包,JS 中要访问 .value。对象可以用 reactive,也可以用 ref 包对象;选择时主要看状态结构和团队习惯。
五、面试追问与工程落地
Vue3 响应式常见追问是“Proxy 是不是递归代理整个对象”。Vue3 通常是懒代理:访问到嵌套对象时,才把嵌套对象转换成响应式代理。这样能避免一开始就深度遍历大对象,降低初始化成本。
还会追问数组和集合类型。Proxy 能拦截数组索引、length、Map/Set 操作等,但 Vue 内部仍然需要对这些数据结构做专门处理,比如追踪迭代依赖、size 依赖、数组方法带来的批量变化。
工程里不要把所有东西都 reactive。大型静态数据、第三方类实例、图表对象、编辑器实例可能不需要深响应式,可以用 markRaw、shallowRef、shallowReactive 控制代理深度,减少无意义追踪。
六、还原依赖桶、清理和调度的完整链路
真实依赖关系可概括为 WeakMap<target, Map<key, Set<effect>>>。WeakMap 不会因为依赖桶持有原始对象而阻止其回收;同一对象的不同 key 各有依赖集合,写 name 时不必让只读取 age 的 effect 重跑。Proxy handler 通常配合 Reflect 保留接收者等 JavaScript 语义。
targetMap (WeakMap)
└─ raw user 对象
└─ depsMap (Map)
├─ "name" → Set[renderEffect, nameComputed]
└─ "age" → Set[renderEffect]
let activeEffect
function track(target, key) {
if (!activeEffect) return
// 取得 target → key 对应 Set,并加入 activeEffect
}
function trigger(target, key) {
// 复制对应 effects,交给 scheduler,避免边遍历边修改
}
条件依赖需要每次执行前清理。effect 第一次读取 ok ? text : fallback 时可能订阅 ok 和 text;ok 变为 false 后应移除旧 text 订阅并新增 fallback 订阅,否则修改已不再使用的 text 仍会触发执行。嵌套 effect 还需要栈保存和恢复当前活动副作用。
| 机制 | 解决的问题 | 代价或边界 |
|---|---|---|
| Proxy get / set | 拦截属性访问和修改 | 代理与原对象身份不同 |
| key 级依赖集合 | 精确触发消费者 | 迭代、数组 length、Map/Set 需特殊 key |
| effect 清理 | 移除过期条件依赖 | 每次执行要维护反向依赖 |
| scheduler | 去重、批量、控制时机 | DOM 不与每次赋值同步 |
| lazy 深层代理 | 避免初始化遍历整棵对象 | 首次访问嵌套值时创建代理 |
假设一个点击同步执行 count++ 3 次,trigger 可以发生 3 次,但 scheduler 将同一组件更新任务放进 Set 去重,最终通常只渲染 1 批并得到值 3。computed、watch 和组件渲染都建立在 effect 与调度能力上,只是惰性、缓存和回调策略不同。
面试主线应完整说成“get 时 track 建图,set 时 trigger 找消费者,scheduler 去重刷新,下一次 effect 执行前清理旧依赖”,不能只停在 Proxy 两个字。
七、常见误区与追问
- 误区:Vue3 创建 reactive 时会立即递归代理所有嵌套对象。 深层转换通常按访问惰性发生,不必初始化时遍历整棵对象。
- 误区:Proxy 自动解决了所有响应式细节。 数组 length、迭代、Map/Set、effect 嵌套和调度都需要框架额外实现。
- 误区:每次 setter 触发都会立即同步重渲染 DOM。 trigger 通知依赖后通常由调度器批量去重,DOM 更新具有队列时机。
- 追问:为什么依赖桶外层用 WeakMap? 原对象不再被其他代码引用时,依赖记录不应单独阻止垃圾回收。
- 追问:条件分支切换后为什么要清理 effect? 否则 effect 会继续订阅已不读取的旧属性,造成无效触发和错误依赖图。
- 追问:新增属性在 Vue3 为什么能被监听? Proxy 可以拦截对象属性设置和删除,不要求像 Vue2 那样初始化时逐属性定义访问器。
- 追问:为什么 reactive(raw) !== raw? reactive 返回代理对象;比较身份、使用 Map key 或与第三方实例交互时要意识到代理身份差异。
八、加强记忆
Vue3 响应式可以记成“读时 track,写时 trigger”。Proxy 负责拦截读写,effect 代表需要重新执行的副作用,调度器负责把多次变化合并成高效更新。
完整链路还包括执行前清理旧依赖,以及 setter 比较新旧值,避免值未变化时制造无效通知。