Vue 和 React 都能做到"数据变化后更新页面",但它们感知变化的方式不同:
Vue 追踪"哪些响应式属性被读取和修改";React 追踪"哪个组件调用了状态更新函数"。
可以先看这张核心对比表:
| 对比维度 | Vue | React |
|---|---|---|
| 响应式入口 | ref、reactive |
useState、useReducer |
| 如何感知变化 | Proxy / getter、setter 拦截属性读写 |
调用 setState 显式通知 React |
| 依赖收集 | 自动收集读取过的响应式属性 | 不追踪普通属性读取 |
| 状态修改方式 | 通常允许直接修改响应式对象 | 通常需要创建新值、替换旧状态 |
| 更新粒度 | 知道具体哪个响应式属性触发组件更新 | 知道某个组件的 state 更新了 |
| 组件更新 | 重新执行组件渲染函数 | 重新执行函数组件 |
| DOM 更新 | 虚拟 DOM 比较并提交变化 | 虚拟 DOM/Fiber 协调并提交变化 |
| 派生状态 | computed 自动收集依赖 |
useMemo 通常显式填写依赖 |
| 副作用 | watchEffect 自动收集,watch 指定来源 |
useEffect 显式填写依赖 |
一、React:显式通知式响应
React 不会监听普通变量或对象属性的修改。
javascript
function Counter() {
let count = 0
function add() {
count++
}
return <button onClick={add}>{count}</button>
}
点击以后,变量确实变了,但页面不会更新,因为 React 不知道发生了变化。
必须调用 React 提供的 setter:
javascript
function Counter() {
const [count, setCount] = useState(0)
function add() {
setCount(count + 1)
}
return <button onClick={add}>{count}</button>
}
setCount 做两件事:
diff
记录下一次状态
+
请求 React 重新渲染当前组件
下一次渲染时,React 会重新调用 Counter(),生成新的 JSX,再把新旧结果进行协调比较。React 官方将这一过程划分为触发渲染、执行渲染和提交 DOM 三个阶段。(React)
可以简化成:
scss
setCount(1)
↓
React 安排更新
↓
Counter() 再执行一次
↓
得到新的 JSX
↓
比较新旧 UI
↓
更新必要的 DOM
二、Vue:依赖追踪式响应
Vue 会把状态转换成响应式对象:
javascript
import { reactive } from 'vue'
const state = reactive({
count: 0
})
Vue 3 对响应式对象主要使用 Proxy。简化原理如下:
vbnet
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
}
})
}
当组件渲染时读取:
state.count
Vue 的 get 拦截器会记录:
当前组件依赖 state.count
以后执行:
state.count++
set 拦截器会通知之前依赖 state.count 的响应式 effect,组件随后重新渲染。Vue 3 对响应式对象使用 Proxy,对 ref 的 .value 使用 getter/setter,并通过 track 和 trigger 完成依赖收集及更新触发。(Vue.js)
过程可以简化为:
javascript
组件读取 state.count
↓
Vue 记录依赖关系
↓
state.count++
↓
Proxy 拦截修改
↓
通知依赖它的组件
↓
组件重新渲染
三、最大的区别:谁负责告诉框架发生了变化
React:开发者显式通知
scss
setCount(count + 1)
React 不关心你之前读取了哪些普通变量。它只知道:
objectivec
某个 state setter 被调用了
当前组件需要重新计算
Vue:框架自动追踪
count.value++
或者:
state.count++
Vue在组件渲染过程中已经记录了:
这个组件读取过 count
所以 count 修改后,它知道应该通知哪些响应式 effect。
因此可以概括为:
React:事件驱动组件更新
Vue:依赖驱动组件更新
不过两者最后通常都会重新执行相应组件的渲染逻辑,并同步必要的 DOM 变化。
四、对象更新方式不同
这是实际开发中最明显的区别之一。
Vue 可以直接修改响应式对象
xml
<script setup>
import { reactive } from 'vue'
const user = reactive({
name: 'Alice',
age: 20
})
function rename() {
user.name = 'Bob'
}
</script>
<template>
<button @click="rename">
{{ user.name }}
</button>
</template>
执行:
ini
user.name = 'Bob'
会经过 Proxy 的 set 拦截,因此可以触发更新。
React 通常不能直接修改状态对象
错误写法:
php
const [user, setUser] = useState({
name: 'Alice',
age: 20
})
function rename() {
user.name = 'Bob'
setUser(user)
}
这里传回去的仍然是原来的对象:
vbnet
Object.is(oldUser, newUser) // true
React 可能认为状态没有变化并跳过更新。
应创建新对象:
javascript
function rename() {
setUser(previous => ({
...previous,
name: 'Bob'
}))
}
React 官方建议把 state 中的对象视为只读数据,通过创建并传入新对象完成更新,而不是直接修改原对象。(React)
所以:
Vue:以可变响应式对象为主
React:以状态快照和不可变更新为主
五、更新粒度有什么区别
假设状态是:
css
const state = {
name: 'Alice',
age: 20,
address: {
city: 'Beijing'
}
}
Vue 的依赖追踪更细
如果模板只读取:
css
<p>{{ state.name }}</p>
Vue 会追踪对 state.name 的读取。
修改:
state.age++
不会触发所有读取 state 的响应式逻辑;Vue知道修改的是 age,而不是 name。
但要注意:
Vue知道哪些属性触发了组件的渲染 effect,不代表它直接绕过组件重新渲染、只修改一个表达式。
当前 Vue 默认渲染系统仍会重新执行相关组件的渲染 effect,然后借助虚拟 DOM 和编译器生成的优化信息减少比较及 DOM 操作。(Vue.js)
React 的触发单位更偏向组件
php
const [user, setUser] = useState({
name: 'Alice',
age: 20
})
调用:
ini
setUser(previous => ({
...previous,
age: previous.age + 1
}))
React知道的是:
sql
这个组件的 user 状态更新了
因此会重新调用这个函数组件。
即使返回的 JSX 中只有一小部分改变,组件函数仍然可能重新执行;但在提交阶段,React只修改真正发生变化的 DOM。(React)
六、派生状态:computed 和 useMemo
Vue 的 computed
csharp
const firstName = ref('Alice')
const lastName = ref('Smith')
const fullName = computed(() => {
return firstName.value + ' ' + lastName.value
})
Vue执行 computed 时会自动记录:
fullName 依赖 firstName.value
fullName 依赖 lastName.value
因此修改其中任意一个:
ini
firstName.value = 'Bob'
都会让 fullName 失效,并在需要时重新计算。Vue 的 computed 内部通过响应式 effect 管理依赖、失效和重新计算。(Vue.js)
React 的 useMemo
javascript
const fullName = useMemo(() => {
return `${firstName} ${lastName}`
}, [firstName, lastName])
依赖通常由开发者显式提供:
csharp
[firstName, lastName]
React不会通过执行回调自动分析你读取了哪些 state。
对比:
Vue computed:运行时自动收集依赖
React useMemo:开发者显式声明依赖
不过 useMemo 的主要定位是性能优化,不应简单地把所有普通计算都包装进去。
七、副作用:watchEffect 和 useEffect
Vue:可以自动收集依赖
scss
watchEffect(() => {
console.log(user.name)
})
执行时读取了:
user.name
Vue自动记录这个依赖。以后 user.name 变化,回调再次执行。
也可以显式指定来源:
javascript
watch(
() => user.name,
newName => {
console.log(newName)
}
)
React:依赖通常显式填写
scss
useEffect(() => {
console.log(user.name)
}, [user.name])
React不会在运行回调时自动通过属性访问收集依赖,而是根据依赖数组判断 effect 是否需要重新执行。
所以一个常见错误是漏掉依赖:
scss
useEffect(() => {
fetchUser(userId)
}, []) // 实际使用了 userId,但没有声明
更合理的是:
scss
useEffect(() => {
fetchUser(userId)
}, [userId])
这体现了两种设计取向:
Vue:依赖发现偏自动化
React:数据流和依赖声明偏显式
八、两者都不是"状态一变就立刻修改 DOM"
Vue 和 React 都通常会对更新进行调度和批处理。
例如:
count.value++
count.value++
count.value++
Vue通常不会同步执行三次完整 DOM 更新。
React 中:
ini
setCount(c => c + 1)
setCount(c => c + 1)
setCount(c => c + 1)
也通常会进入更新队列,并被批量处理。React明确说明,setter 请求下一次渲染,多个更新可以被排队和批处理。(React)
两者大致都是:
markdown
修改状态
↓
将更新放入调度队列
↓
合并同一批更新
↓
重新计算 UI
↓
一次性提交必要 DOM 变化
因此,在 Vue 中修改数据后立刻读取真实 DOM,也可能仍然看到旧结果,通常需要等待 nextTick()。
九、二者都使用虚拟 DOM,但 Vue 更依赖编译器优化
React 中,JSX运行后产生 React 元素描述,React通过协调过程比较新旧结构并提交 DOM 变化。
Vue模板:
css
<div>
<h1>Static title</h1>
<p>{{ count }}</p>
</div>
会先被编译为渲染函数。由于 Vue能够分析模板,它可以提前识别:
css
<h1>Static title</h1> 是静态的
<p>{{ count }}</p> 中的文本是动态的
然后通过静态提升、patch flags 等方式缩小运行时需要检查的范围。Vue官方把它描述为"编译器信息虚拟 DOM",即编译器和运行时协作进行优化。(Vue.js)
因此不能简单说:
Vue 不使用虚拟 DOM
React 使用虚拟 DOM
当前主流 Vue 3 渲染模式同样使用虚拟 DOM,只是它结合了模板编译期信息。
十、两者常见陷阱不同
Vue:解构可能丢失响应性
php
const state = reactive({
count: 0
})
const { count } = state
这里的 count 是一个普通局部变量:
count++
不会再经过 state 的 Proxy。
可以使用:
scss
const { count } = toRefs(state)
count.value++
Vue官方说明,从响应式对象中把某个属性赋给或解构到局部变量时,该局部变量绑定不再触发源对象的代理拦截。(Vue.js)
React:闭包拿到旧状态快照
scss
function handleClick() {
setCount(count + 1)
setTimeout(() => {
console.log(count)
}, 1000)
}
定时器看到的是创建当前事件处理函数时那次渲染的 count 快照,不是未来自动变化的值。React每一次渲染都有自己的 state 快照和事件处理函数。(React)
可以理解为:
Vue 常见问题:响应式连接是否还存在
React 常见问题:当前闭包属于哪一次渲染
十一、完整流程对比
Vue
markdown
1. 组件执行渲染函数
2. 渲染时读取响应式属性
3. Proxy/ref 收集依赖
4. 修改响应式属性
5. trigger 通知相关 effect
6. 调度组件更新
7. 重新执行渲染函数
8. 比较新旧虚拟 DOM
9. 提交必要 DOM 修改
React
markdown
1. 组件函数执行
2. 得到当前 JSX 快照
3. 调用 state setter
4. React 将更新加入队列
5. 调度组件重新渲染
6. 组件函数重新执行
7. 得到新的 JSX 快照
8. 协调新旧树
9. 提交必要 DOM 修改
最终记忆方式
可以这样记:
Vue:
我读取了哪些响应式数据?
这些数据变化时,自动通知我。
React:
我调用了哪个状态更新函数?
对应组件需要重新计算一次。
再进一步:
sql
Vue 的核心:
Proxy / ref + track + trigger + reactive effect
React 的核心:
state snapshot + setter + render + reconciliation + commit
Vue更强调自动依赖追踪和可变响应式状态 ;React更强调显式状态更新、不可变数据和组件重新计算。两者最终都会尽量只把必要的变化提交到真实 DOM。