← 返回题目列表

Vue3 响应式原理是什么?

高频 困难 第 17 / 27 题 更新于 2026/07/27
VueVue3响应式Proxy

简化版

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。大型静态数据、第三方类实例、图表对象、编辑器实例可能不需要深响应式,可以用 markRawshallowRefshallowReactive 控制代理深度,减少无意义追踪。

六、还原依赖桶、清理和调度的完整链路

真实依赖关系可概括为 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 时可能订阅 oktext;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 比较新旧值,避免值未变化时制造无效通知。