横向对比 / 闯关模式
React vs Vue 心智模型对比
从 render/commit、状态快照、Proxy 响应式、依赖追踪和调度更新理解 React 与 Vue 的底层差异。
一句话:React 的核心是“状态变化后重新执行组件函数,算出新的 UI”,Vue 的核心是“响应式数据被模板使用后建立依赖,数据变化时精准通知相关 UI 更新”。
第 8 篇 / 共 12 篇。
本篇学完你会:用同一个用户管理后台案例,理解 React 和 Vue 的渲染原理、响应式原理、更新流程、数据流、Hooks/Composition API 差异,而不是只记住表层语法。
1. 为什么心智模型比语法更重要
语法只是表面。
你看到 React 写:
setKeyword(event.target.value);Vue 写:
<input v-model="keyword" />如果只背语法,你会觉得:
React 麻烦,Vue 简单。但真实项目里更重要的问题是:
为什么 React 要 setState?
为什么 Vue 改 keyword 就能自动更新?
为什么 React 要写依赖数组?
为什么 Vue 的 computed 能自动知道依赖?
为什么 React 状态要当快照看?
为什么 Vue reactive 解构后可能丢响应式?这些才是心智模型。
心智模型就是你脑子里对框架运行方式的“地图”。地图对了,你遇到 bug 能自己推;地图错了,就只能到处搜答案。

2. 先用一句话区分 React 和 Vue
React:
状态变了 -> 重新执行组件函数 -> 得到新的 UI 描述 -> React 对比差异 -> 更新 DOMVue:
模板读取响应式数据 -> Vue 记录依赖 -> 数据变了 -> 通知依赖它的地方 -> 更新 DOM更大白话一点:
| 框架 | 像什么 | 重点 |
|---|---|---|
| React | 每次重新算一遍账单 | 你告诉它状态变了,它重新计算 UI |
| Vue | 在数据上贴监听器 | 谁用过这个数据,数据变了就通知谁 |
这就是两者最根本的差异。
3. 同一个用户管理页,两种思考入口
我们的案例还是用户管理后台:
搜索用户
渲染用户列表
打开编辑弹窗
保存表单
刷新列表
根据权限显示按钮React 会更自然地问:
这个页面有哪些 state?
每个 state 变化后,UI 应该重新算成什么样?
哪些逻辑要放进 Hook?
哪些 props 要传给子组件?Vue 会更自然地问:
模板需要绑定哪些数据?
哪些数据用 ref,哪些数据用 reactive?
哪些逻辑抽成 composable?
哪些变化要 watch?同一个搜索框:
React:
const [keyword, setKeyword] = useState('');
<input value={keyword} onChange={(event) => setKeyword(event.target.value)} />;Vue:
const keyword = ref('');<input v-model="keyword" />表面上看只是写法不同,底层其实是两套更新模型。
4. React 的原理:重新执行组件函数
React 函数组件可以看成一个普通函数:
function UserManagementPage() {
const [keyword, setKeyword] = useState('');
return <UserTable keyword={keyword} />;
}React 的核心动作是:
执行函数 -> 拿到 JSX -> 生成 UI 描述 -> 提交到真实 DOM当你调用:
setKeyword('小明');React 不会直接去改某个 <input> 或某个 <tr>。
它会安排一次更新:
1. keyword 的下一次状态变成 "小明"
2. React 重新执行 UserManagementPage()
3. 得到新的 JSX 结果
4. React 比较新旧 UI 描述
5. 只把必要变化提交到 DOMReact 里的 UI 是“结果”,不是“命令”
你不是在命令浏览器:
把第 3 行改成小明
把按钮变蓝
把弹窗打开你是在告诉 React:
当 keyword 是小明时,页面应该长这样。
当 open 是 true 时,弹窗应该显示。
当 permissions 包含 user:create 时,新增按钮应该出现。React 根据状态重新算 UI。
React 的更新大概分两步
可以粗略理解成:
Render 阶段:重新计算新的 UI 描述
Commit 阶段:把变化提交到真实 DOMReact 内部还有 Fiber、调度、优先级、批处理等机制。初学不需要马上钻到源码,但要知道:React 不是你 set 一次就立刻同步改完全部 DOM,它会调度更新、合并更新、再提交变化。
React 的关键心智词
| 词 | 大白话 |
|---|---|
| state snapshot | 每次渲染拿到的是那一次的状态快照 |
| render | 执行组件函数,算出 UI |
| reconciliation | 比较新旧 UI 描述 |
| commit | 把差异更新到真实 DOM |
| batching | 多次状态更新合并成一次渲染 |
5. Vue 的原理:响应式依赖追踪
Vue 的核心不是“每次你手动告诉它重新算整个组件”,而是“它知道谁用过哪些响应式数据”。
Vue 里:
const keyword = ref('');
const users = ref<User[]>([]);模板里:
<input v-model="keyword" />
<UserTable :users="users" />当模板渲染时,Vue 会读取 keyword 和 users。读取的时候,Vue 就有机会记录:
这个模板依赖 keyword。
这个表格依赖 users。以后:
keyword.value = '小明';Vue 就知道:
keyword 变了,通知依赖 keyword 的地方更新。track 和 trigger
Vue 响应式原理可以用两个词理解:
| 词 | 大白话 |
|---|---|
| track | 读取响应式数据时,记录“谁用到了我” |
| trigger | 修改响应式数据时,通知“用到我的地方” |
简化版:
读取 keyword -> track(keyword)
修改 keyword -> trigger(keyword)ref 和 reactive
ref 像一个盒子:
const keyword = ref('');
keyword.value = '小明';reactive 像一个被代理的对象:
const query = reactive({
keyword: '',
status: 'all'
});
query.keyword = '小明';Vue 通过 Proxy 代理对象的读取和修改,从而做依赖追踪。
Vue 的更新大概分几步
可以粗略理解成:
1. 模板渲染时读取响应式数据
2. Vue 记录依赖关系
3. 响应式数据变化
4. Vue 触发相关 effect
5. 调度更新
6. patch DOMVue 也有虚拟 DOM 和 patch 过程,但它的心智入口通常是响应式依赖。
6. 搜索框输入时,两边到底发生了什么
用户在搜索框输入“小明”。
React 流程
1. 用户输入
2. onChange 触发
3. setKeyword('小明')
4. React 安排状态更新
5. 组件函数重新执行
6. keyword 这次变成 "小明"
7. 根据新 keyword 计算 filteredUsers
8. React 对比新旧 UI
9. DOM 更新代码:
const [keyword, setKeyword] = useState('');
const filteredUsers = users.filter((user) => user.name.includes(keyword));
<input value={keyword} onChange={(event) => setKeyword(event.target.value)} />;Vue 流程
1. 用户输入
2. v-model 更新 keyword
3. keyword.value 变化
4. Vue trigger keyword 的依赖
5. computed 重新计算 filteredUsers
6. 模板相关部分更新
7. DOM patch代码:
const keyword = ref('');
const filteredUsers = computed(() => users.value.filter((user) => user.name.includes(keyword.value)));<input v-model="keyword" />核心差异
| 问题 | React | Vue |
|---|---|---|
| 输入如何更新数据 | onChange -> setKeyword | v-model -> keyword.value |
| 谁知道要重算 | React 重新执行组件 | Vue 依赖追踪触发 computed/template |
| 依赖怎么声明 | 很多地方要手动写依赖 | 响应式读取时自动收集 |
7. 用户列表刷新时,两边如何更新 DOM
接口返回新用户列表:
[
{ id: 1, name: '小明' },
{ id: 2, name: '小红' }
]React
setUsers(nextUsers);React 会重新执行组件:
users.map((user) => <UserRow key={user.id} user={user} />);然后通过 key 判断:
哪个用户是原来就有的?
哪个用户是新增的?
哪个用户被删除了?
哪个用户内容变了?Vue
users.value = nextUsers;Vue 触发依赖 users 的模板更新:
<UserRow v-for="user in users" :key="user.id" :user="user" />Vue 也会通过 key 做列表 patch。
共同点
两者都不是每次粗暴清空整个页面再重建。
它们都会尽量复用 DOM,只更新必要变化。
所以无论 React 还是 Vue,列表都要写稳定 key:
<UserRow key={user.id} user={user} /><UserRow :key="user.id" :user="user" />8. 为什么 React 强调不可变更新
React 推荐你不要直接改旧对象:
// 不推荐
query.keyword = '小明';
setQuery(query);推荐:
setQuery((current) => ({
...current,
keyword: '小明'
}));原因是 React 更容易通过“引用是否变化”判断状态变没变。
大白话:
旧对象还是那个对象,React 很难知道里面是不是被偷偷改了。
给一个新对象,React 一看引用变了,就知道要更新。这也是为什么 React 里经常看到:
setUsers((current) => current.map((user) => (user.id === id ? { ...user, status: 'disabled' } : user)));它不是故意绕,是在保持数据变化路径清晰。
9. 为什么 Vue 可以直接改响应式对象
Vue 里常见写法:
query.keyword = '小明';它可以这样,是因为 query 是响应式代理对象。Vue 能拦截这次赋值:
你改了 query.keyword。
我知道。
我通知依赖 query.keyword 的地方。这就是 Proxy 的价值。
但是 Vue 也有坑:
const { keyword } = query;如果你随便解构,可能会丢掉响应式连接。更稳的方式是:
const { keyword } = toRefs(query);大白话:
Vue 允许你像改普通对象一样写代码,但前提是你别把响应式对象拆坏。10. JSX vs Template 的底层差异
React JSX
JSX 本质会变成 JavaScript 调用。
你写:
<UserRow user={user} />可以理解成创建一个 UI 描述对象。
所以 React 的 UI 逻辑天然离 JS 很近:
{users.length > 0 ? <UserTable users={users} /> : <Empty />}条件、循环、函数、变量,都用 JS 处理。
Vue Template
Vue template 会先被编译器编译。
你写:
<UserRow v-for="user in users" :key="user.id" :user="user" />Vue 编译器会分析模板结构,并生成渲染函数。
它可以在编译阶段做一些优化,比如知道哪些节点是静态的,哪些地方依赖动态数据。
对比
| 对比点 | React JSX | Vue Template |
|---|---|---|
| 本质 | JS 里的 UI 表达式 | 模板被编译成渲染函数 |
| 动态能力 | 完整 JavaScript | 指令 + 表达式 |
| 优化入口 | 运行时和编译器配合,React Compiler 方向增强 | 编译器天然能分析模板 |
| 初学体验 | 要先适应 JSX | 更接近 HTML |
不是谁绝对好,而是入口不同。
React 把“写 UI”交给 JavaScript;Vue 把“写 UI”交给模板和编译器。
11. Hooks vs Composition API 的原理差异
它们都能复用逻辑,但运行规则不同。
React Hook
function useUsers() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(false);
return { users, loading };
}React Hook 依赖调用顺序。
也就是说,不要这样:
if (enabled) {
const [users, setUsers] = useState([]);
}因为 React 需要按固定顺序对应每个 Hook 的状态位置。
大白话:
React 每次执行组件函数,都要按同样顺序拿状态。
顺序乱了,就不知道哪个状态对应哪个 useState。Vue composable
export function useUsers() {
const users = ref<User[]>([]);
const loading = ref(false);
return { users, loading };
}Vue composable 本质就是普通函数,里面创建并返回响应式数据。它没有 React Hook 那种严格的调用顺序规则,但你仍然应该在 setup 阶段稳定调用,代码才清楚。
对比
| 对比点 | React Hook | Vue composable |
|---|---|---|
| 本质 | 组件渲染过程中的状态槽位机制 | 创建并组合响应式数据的普通函数 |
| 规则 | 必须顶层调用,顺序稳定 | 更灵活,但建议 setup 中稳定调用 |
| 依赖 | 依赖数组、闭包、状态快照很重要 | 响应式追踪、ref 解包很重要 |
| 常见坑 | stale closure、依赖漏写 | 解构丢响应式、watch 过深 |
12. useEffect vs watch/onMounted 的差异
React:
useEffect(() => {
loadUsers(query);
}, [query]);这句话的意思是:
这次渲染完成后,如果 query 变了,就执行这个副作用。Vue:
watch(
() => ({ ...query }),
() => {
loadUsers();
}
);这句话的意思是:
观察 query 相关数据,变了就执行 loadUsers。首次加载:
React:
useEffect(() => {
loadUsers();
}, []);Vue:
onMounted(() => {
loadUsers();
});| 问题 | React | Vue |
|---|---|---|
| 副作用依赖谁 | 依赖数组声明 | watch 源声明或响应式自动追踪 |
| 首次挂载 | useEffect(..., []) | onMounted |
| 清理 | effect 返回函数 | onUnmounted 或 watch cleanup |
| 典型坑 | 闭包拿到旧值 | watch 太频繁 |
13. computed vs useMemo 的差异
Vue:
const activeUsers = computed(() => users.value.filter((user) => user.status === 'active'));Vue 会自动知道:
activeUsers 依赖 users.value。
users 变了,activeUsers 才重新算。React:
const activeUsers = useMemo(() => users.filter((user) => user.status === 'active'), [users]);React 需要你告诉它:
这个计算依赖 users。
users 变了才重新算。核心区别
| 对比点 | React useMemo | Vue computed |
|---|---|---|
| 依赖来源 | 手动写依赖数组 | 自动追踪响应式读取 |
| 用途 | 缓存昂贵计算 | 声明派生状态 |
| 心智 | “这些依赖变了再算” | “我用到的数据变了再算” |
不要把 computed 和 useMemo 当成接口请求工具。它们都应该用来算值。
14. 组件通信心智模型
父传子,两边都叫 props。
React:
<UserRow user={user} />Vue:
<UserRow :user="user" />子传父不同。
React:
<UserRow user={user} onEdit={openEditModal} />Vue:
<UserRow :user="user" @edit="openEditModal" />React 里,“子传父”本质是子组件调用父组件传来的函数。
Vue 里,“子传父”本质是子组件发出一个事件,父组件监听这个事件。
| 对比点 | React | Vue |
|---|---|---|
| 父传子 | props | props |
| 子传父 | callback props | emit event |
| 内容分发 | children | slot |
| 心智 | 函数调用更直接 | 事件语义更清楚 |
15. 全局状态心智模型
当前登录用户和权限列表,多个页面都要用,所以适合全局。
React 常见选择:
Context:少量全局数据
Zustand:中小项目业务状态
Redux Toolkit:大型团队强规范Vue 常见选择:
Pinia:Vue 官方推荐状态管理React Zustand:
const canCreate = useAuthStore((state) => state.permissions.includes('user:create'));Vue Pinia:
const authStore = useAuthStore();
const canCreate = computed(() => authStore.permissions.includes('user:create'));共同原则:
不是所有 state 都要全局。
搜索框输入、编辑表单草稿,通常留在页面或组件本地。
当前用户、权限、主题、菜单,才更适合全局。16. TypeScript 心智模型
React 更像“纯 TS 函数 + JSX”:
type UserTableProps = {
users: User[];
onEdit: (user: User) => void;
};
function UserTable({ users, onEdit }: UserTableProps) {
return <table>{/* ... */}</table>;
}Vue 更像“宏 + 模板类型推导”:
const props = defineProps<{
users: User[];
}>();
const emit = defineEmits<{
edit: [user: User];
}>();| 对比点 | React | Vue |
|---|---|---|
| 组件类型 | 函数参数类型 | defineProps / defineEmits |
| 事件类型 | React event 类型 | DOM Event 或 emit 类型 |
| 模板类型 | JSX 直接走 TS | Vue 模板由工具推导 |
| 适应成本 | 熟 TS 会比较顺 | 要熟悉 Vue SFC 宏 |
17. 性能优化心智模型
React 性能问题常来自:
组件重新执行
props 引用变化
昂贵计算重复执行
子组件没必要重渲染常见工具:
useMemo
useCallback
memo
列表虚拟滚动
路由懒加载Vue 性能问题常来自:
响应式对象太大
watch 太深
列表太大
组件拆分不合理
不必要的全局状态依赖常见工具:
computed
shallowRef
markRaw
分页/虚拟滚动
路由懒加载共同原则:
先把数据流写清楚,再优化。
不要为了看起来专业,提前堆 useMemo、watch、缓存。18. 常见误解
| 误解 | 更准确的说法 |
|---|---|
| React 每次 setState 都重建整个 DOM | React 会重新计算 UI 描述,再对比差异更新 DOM |
| Vue 不需要理解更新原理 | Vue 的响应式很方便,但解构、watch、对象边界都需要理解 |
| React 比 Vue 高级 | 两者解决问题方式不同,不是等级关系 |
| Vue 只是模板语法糖 | Vue 背后有编译器、响应式系统、调度和 patch |
| JSX 一定难维护 | 逻辑复杂时 JSX 很灵活 |
| Template 一定不灵活 | Vue 的指令、slot、render function 也能处理复杂场景 |
| computed 和 useMemo 完全一样 | 目标相似,但依赖追踪方式不同 |
| Hook 和 composable 完全一样 | 都能复用逻辑,但运行规则和响应式模型不同 |
19. 选择和迁移建议
如果你从 Vue 学 React,重点补:
JSX
不可变更新
state snapshot
useEffect 依赖数组
Hook 调用规则
props callback如果你从 React 学 Vue,重点补:
template 指令
ref/reactive
computed/watch
script setup
defineProps/defineEmits
Pinia
响应式解构边界如果你两个都学,建议用同一个用户管理后台案例做两遍:
同一个搜索框
同一个列表
同一个编辑弹窗
同一个权限按钮
同一个接口请求
同一个状态管理做完后你会发现:框架语法不同,但优秀前端代码的底层能力很像。
20. 下一步怎么学
本篇讲的是“为什么两者这样设计”。下一篇会更细地对比具体语法和真实开发任务:
文本怎么写
class 怎么绑
事件怎么绑
表单怎么处理
props/emit 怎么对应
请求怎么组织
路由权限怎么落地
工程目录怎么设计