响应式模型与组件对照
Vue vs React 心智模型对照 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
这是整条 Vue track 的收口课。学完你要能在一张表里说清「同一个需求在 Vue 和 React 里分别怎么写、为什么这样写」,并理解两种模型的代价与收益——不是记住 API,是理解设计取舍。
问题场景
面试官问「Vue 和 React 的区别」,你答「Vue 是模板,React 是 JSX」。这个答案对了 10%。真正的差异在响应式机制、更新模型、以及由此衍生的一切:为什么 Vue 不需要 useMemo、为什么 React 需要不可变更新、为什么 Vue 的 ref 要分 .value、为什么 React 的闭包会拿到旧状态。这些问题都能追溯到同一个源头。
核心解释
源头:状态变化的检测方式。
React 是「不可变快照 + 重新渲染」:状态是值,改状态必须 setState 提供新值,组件函数整体重跑,用虚拟 DOM diff 找最小变更。代价是「读取的是快照」(闭包问题)、需要手动稳定引用(useMemo/useCallback)。
Vue 是「可变对象 + 依赖追踪」:状态是 Proxy 包装的对象,属性被读写时记录依赖,变化时精确通知用过它的渲染副作用。代价是「解构会丢追踪」(V03)、「响应式边界需要理解」(shallowRef)。
| 维度 | React | Vue 3 |
|---|---|---|
| 状态更新 | setState(新值),不可变 | state.value++ / state.x = 值,可变 |
| 重新渲染范围 | 组件函数整体重跑 | 依赖变了才重跑渲染副作用 |
| 派生状态 | 每次渲染重算,需要 useMemo 缓存 | computed 自动缓存 + 追踪 |
| 副作用 | useEffect + 依赖数组 | watch / watchEffect + 自动追踪 |
| 事件 | 合成事件,函数引用需要 useCallback | 原生事件委托,无稳定引用问题 |
| 模板 | JSX(JS 表达力) | 模板(编译期优化,如静态提升) |
| 跨层通信 | Context | provide/inject |
| 全局状态 | Zustand/Redux + Context | Pinia |
| 路由 | React Router | Vue Router |
| 全栈框架 | Next.js | Nuxt |
同一需求的三种对照。计数器:
// React
const [count, setCount] = useState(0);
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<!-- Vue -->
const count = ref(0);
<button @click="count++">{{ count }}</button>
派生值:React 每次渲染重算,依赖复杂时用 useMemo 缓存;Vue 写 computed(() => items.value.reduce((s, it) => s + it.price * it.qty, 0)),自动缓存、自动追踪依赖,不需要手动声明依赖数组。
异步数据:React 用 useEffect 加依赖数组与清理函数,容易漏依赖;Vue 用 watch(url, async (u, _o, onCleanup) => { const ac = new AbortController(); onCleanup(() => ac.abort()); ... }, { immediate: true }),或者直接抽成 useFetch composable(V13)。
心智模型的关键差异:React 里你思考「这次渲染的状态快照是什么」;Vue 里你思考「谁依赖了这个值」。这决定了两边的常见 bug 完全不同——React 的 bug 多是「读了旧快照」,Vue 的 bug 多是「解构丢了追踪」。
示例拆解
一个真实组件的双版本对照,同一个「搜索 + 结果」功能:
// React 版
function Search() {
const [keyword, setKeyword] = useState("");
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!keyword) { setResults([]); return; }
const ac = new AbortController();
setLoading(true);
fetch(`/api/search?q=${encodeURIComponent(keyword)}`, { signal: ac.signal })
.then(r => r.json())
.then(setResults)
.catch(e => { if (e.name !== "AbortError") console.error(e); })
.finally(() => setLoading(false));
return () => ac.abort();
}, [keyword]);
return (
<div>
<input value={keyword} onChange={e => setKeyword(e.target.value)} />
{loading ? <p>加载中…</p> : <ul>{results.map(r => <li key={r.id}>{r.title}</li>)}</ul>}
</div>
);
}
<!-- Vue 版 -->
<script setup>
import { computed, ref } from "vue";
import { useFetch } from "../composables/useFetch";
const keyword = ref("");
const { data: results, loading } = useFetch(
() => (keyword.value ? `/api/search?q=${encodeURIComponent(keyword.value)}` : ""),
{ immediate: true }
);
</script>
<template>
<div>
<input v-model="keyword" />
<p v-if="loading">加载中…</p>
<ul v-else>
<li v-for="r in results" :key="r.id">{{ r.title }}</li>
</ul>
</div>
</template>
对照阅读:React 版用一个 effect 承担「依赖跟踪 + 取消 + 三态」;Vue 版把这套模式封进 useFetch,组件只剩声明。这不是谁更好的问题——React 的显式让你看清每一步,Vue 的自动让你少写样板,代价是「自动」出问题时(比如 URL getter 忘了返回值)更难排查。
动手任务
- 把本站的 C04(React 计数器)用 Vue 重写一遍,行数对比,说出少在哪里、多在哪里。
- 给 V04 的 TodoItem 写一份 React 版,对照 props/emits 与 props/onXxx 的契约差异。
- 写一段 300 字的「Vue vs React」解释,要求出现:依赖追踪、不可变快照、computed 缓存、闭包旧值 四个关键词。
常见错误
- 用 React 思维写 Vue:手动 watch 维护派生状态(应 computed)、害怕直接改对象(Vue 就是让你改的)、到处 useMemo 等价物(不需要)。
- 用 Vue 思维写 React:直接改 state 对象属性(不触发更新)、依赖数组漏项(读了旧闭包)。
- 把「模板 vs JSX」当核心差异:真正的差异是更新模型;模板/JSX 只是表层语法。
- 认为响应式等于性能一定更好:精确更新减少重渲染,但 Proxy 有创建与追踪成本;大对象场景反而需要 shallowRef(V15)。
检查与复盘
- 能说出两种更新模型的根本差异及其三个衍生后果
- 能写出计数器、派生值、异步请求的三组对照
- 能指出「读旧快照」与「解构丢追踪」各自的成因
参考资料
- Vue 官方文档「与 React 的对比」:官方的框架对照
- Vue 官方文档「组合式 API 常见问答」:设计动机
- React 官方文档「管理状态」:不可变更新的理由
读完这一课了?
标记完成后,工作台会安排接下来的学习。