横向对比 / 闯关模式

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 能自己推;地图错了,就只能到处搜答案。

React 和 Vue 心智模型对比图

2. 先用一句话区分 React 和 Vue

React:

状态变了 -> 重新执行组件函数 -> 得到新的 UI 描述 -> React 对比差异 -> 更新 DOM

Vue:

模板读取响应式数据 -> 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. 只把必要变化提交到 DOM

React 里的 UI 是“结果”,不是“命令”

你不是在命令浏览器:

把第 3 行改成小明
把按钮变蓝
把弹窗打开

你是在告诉 React:

当 keyword 是小明时,页面应该长这样。
当 open 是 true 时,弹窗应该显示。
当 permissions 包含 user:create 时,新增按钮应该出现。

React 根据状态重新算 UI。

React 的更新大概分两步

可以粗略理解成:

Render 阶段:重新计算新的 UI 描述
Commit 阶段:把变化提交到真实 DOM

React 内部还有 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 会读取 keywordusers。读取的时候,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 DOM

Vue 也有虚拟 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" />

核心差异

问题ReactVue
输入如何更新数据onChange -> setKeywordv-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 JSXVue 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 HookVue 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();
});
问题ReactVue
副作用依赖谁依赖数组声明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 useMemoVue computed
依赖来源手动写依赖数组自动追踪响应式读取
用途缓存昂贵计算声明派生状态
心智“这些依赖变了再算”“我用到的数据变了再算”

不要把 computeduseMemo 当成接口请求工具。它们都应该用来算值。

14. 组件通信心智模型

父传子,两边都叫 props。

React:

<UserRow user={user} />

Vue:

<UserRow :user="user" />

子传父不同。

React:

<UserRow user={user} onEdit={openEditModal} />

Vue:

<UserRow :user="user" @edit="openEditModal" />

React 里,“子传父”本质是子组件调用父组件传来的函数。

Vue 里,“子传父”本质是子组件发出一个事件,父组件监听这个事件。

对比点ReactVue
父传子propsprops
子传父callback propsemit event
内容分发childrenslot
心智函数调用更直接事件语义更清楚

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];
}>();
对比点ReactVue
组件类型函数参数类型defineProps / defineEmits
事件类型React event 类型DOM Event 或 emit 类型
模板类型JSX 直接走 TSVue 模板由工具推导
适应成本熟 TS 会比较顺要熟悉 Vue SFC 宏

17. 性能优化心智模型

React 性能问题常来自:

组件重新执行
props 引用变化
昂贵计算重复执行
子组件没必要重渲染

常见工具:

useMemo
useCallback
memo
列表虚拟滚动
路由懒加载

Vue 性能问题常来自:

响应式对象太大
watch 太深
列表太大
组件拆分不合理
不必要的全局状态依赖

常见工具:

computed
shallowRef
markRaw
分页/虚拟滚动
路由懒加载

共同原则:

先把数据流写清楚,再优化。
不要为了看起来专业,提前堆 useMemo、watch、缓存。

18. 常见误解

误解更准确的说法
React 每次 setState 都重建整个 DOMReact 会重新计算 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 怎么对应
请求怎么组织
路由权限怎么落地
工程目录怎么设计

下一篇建议:大白话讲解——React vs Vue 真实开发任务对比.md