响应式系统对比:Vue3 vs React 深度解析
Vue3 用 Proxy 自动追踪谁读了谁,变化时精确更新依赖方;React 用不可变数据加显式 setState 触发整棵组件树重跑再靠 Diff 找最小变化。 前者是我帮你追踪,后者是你给我新快照,面试常考两套响应式范式的本质差异与各自取舍。
一句话概括
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 3 | React | |
|---|---|---|
| 原理 | 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,后者没有——但这不代表孰优孰劣,只是场景不同、取舍不同。