文章

响应式系统对比:Vue3 vs React 深度解析

Vue3 用 Proxy 自动追踪谁读了谁,变化时精确更新依赖方;React 用不可变数据加显式 setState 触发整棵组件树重跑再靠 Diff 找最小变化。 前者是我帮你追踪,后者是你给我新快照,面试常考两套响应式范式的本质差异与各自取舍。

响应式系统对比:Vue3 vs React 深度解析

一句话概括

Vue 3 用 Proxy 自动追踪”谁读了谁”,变化时精确更新依赖方;React 用不可变数据 + 显式 setState 触发整棵组件树重跑,再靠虚拟 DOM Diff 找出最小变化——前者是”我帮你追踪”,后者是”你给我新快照”。

核心知识点

1. Vue 3 Proxy:自动依赖收集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Vue 3 reactive 的简化核心
function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      track(target, key);  // 当前副作用(如渲染)记录为依赖
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver);
      trigger(target, key);  // 通知所有依赖该 key 的副作用重新执行
      return result;
    }
  });
}

const state = reactive({ count: 0 });
// 渲染函数读了 state.count → track 记录依赖
// state.count = 1 → trigger 只更新依赖 count 的组件

更新粒度:组件级。 数据变了哪个组件用了就更新哪个,不需要从根开始 Diff。

2. React 不可变数据:显式触发

1
2
3
4
5
6
7
// React:你必须调用 setState,React 才"知道"数据变了
const [state, setState] = useState({ count: 0 });
// state.count = 1 ❌ React 不知道!
setState({ count: 1 })  // ✅ 触发从根组件开始的 reconciliation

// React 依赖"全量新快照"做 Diff
// state 是快照 A,setState 后产生快照 B,Diff(A, B) → 最小 DOM 更新

React 不能像 Vue 那样直接改数据就能自动更新——因为它没有 Proxy 拦截,无法感知对象属性的变更。所以 React 强制不可变数据:每次更新都创建新对象/数组。

3. 核心差异对比

 Vue 3React
原理Proxy 拦截 get/set不可变数据 + setState
依赖追踪自动(read 时 track)手动声明(deps 数组 / 编译器推断)
更新粒度组件级(精确到用到该数据的组件)子树级(从根 re-render,靠 memo 跳过)
数据修改直接改:state.count++必须新建:setState({...state, count: state.count+1})
性能默认值开箱即用需手动 useMemo/React.memo 优化
调试DevTools 可追踪每个属性的依赖图DevTools Profiler 看渲染火焰图

4. Proxy 的坑:必须通过代理对象访问

1
2
3
4
5
6
7
8
9
10
11
const state = reactive({ count: 0 });
let { count } = state;  // ❌ 解构丢掉了响应性!
count++;                // state.count 还是 0

// ✅ 用 ref 保持响应性
const count = ref(0);
count.value++;          // 触发更新

// ✅ reactive 用 toRefs 保持
const { count } = toRefs(state);
count.value++;          // 触发更新

Vue 3 的 ref 就是为了解决 Proxy 不能代理基本类型的问题。ref 内部是一个 { value: xxx } 的 reactive 对象。

5. React 不可变数据的更新模式

1
2
3
4
5
6
7
8
9
10
// 数组
setItems(prev => [...prev, newItem]);           // 追加
setItems(prev => prev.filter(t => t.id !== id)); // 删除
setItems(prev => prev.map(t => t.id === id ? {...t, done: true} : t)); // 更新

// 嵌套对象
setUser(prev => ({
  ...prev,
  profile: { ...prev.profile, name: 'New' }
}));

React 依赖引用变化判断是否需要更新。直接 prev.items.push(x) → 引用不变 → React 认为没变化 → 跳过更新。

其实你每天都在用

  • Vue 的 computed:const doubled = computed(() => count.value * 2) — 自动追踪 count,count 不变就返回缓存
  • Vue 的 watchEffect:watchEffect(() => console.log(state.a + state.b)) — a 或 b 任何变化都自动重新执行
  • React 的 setState 批处理:连续 3 次 setCount(c => c + 1) 只渲染一次,底层是更新队列合并
  • React 的 useMemo:手动告诉 React “这个值依赖 [a, b],它们不变就跳过计算” — 在 Vue 3 这是自动的

常见误解(FAQ)

  • ❌ 误区:「Vue 的响应式比 React 快」 不一定。Vue 在”精确更新”上省了 Diff 时间,但 Proxy 的 get/set 拦截本身有开销。React 的虚拟 DOM Diff 虽然”粗放”但极快(纯 JS 对象比较)。实际性能差异取决于场景,大型列表两者都得上虚拟滚动。

  • ❌ 误区:「React 每次 setState 都渲染整棵树」 React 有 bailout 机制。如果父组件重渲染但子组件的 props/state 没变(配合 React.memo),子组件跳过渲染。Vue 也有类似机制(v-memo)。

  • ❌ 误区:「Proxy 能拦截一切」 不能。Proxy 无法拦截 Map / Set 的原生操作(如 map.set()),Vue 3 为这些数据类型专门做了处理。另外 Proxy 不支持 IE(Vue 3 放弃了 IE 兼容)。

  • ❌ 误区:「React 不能直接改 state」是设计缺陷 这是刻意设计。不可变数据让 React 可以用简单的 === 判断是否变化(而不是深度比较),同时天然支持时间旅行调试和并发渲染的快照隔离。

一句话总结

Vue 3 说”你随便改,我帮你追踪”;React 说”你给我新快照,我帮你看哪里变了”。两种哲学的差异根源在于前者有 Proxy,后者没有——但这不代表孰优孰劣,只是场景不同、取舍不同。

本文由作者按照 CC BY 4.0 进行授权