React vs Vue:源码深度对比
基于 React 19 和 Vue 3.5 源码深度分析,从设计哲学到实现细节,全方位对比两大框架的核心差异。一文搞懂为什么 React 选择不可变,Vue 选择响应式。
2024-2025 新特性速览
React 19 重大更新
function UpdateName() {
const [error, submitAction, isPending] = useActionState(
async (prevState, formData) => {
const error = await updateName(formData.get("name"));
if (error) return error;
redirect("/profile");
},
null
);
return (
<form action={submitAction}>
<input name="name" />
<button disabled={isPending}>Update</button>
{error && <p>{error}</p>}
</form>
);
}
function LikeButton({ initialLikes }) {
const [likes, setLikes] = useState(initialLikes);
const [optimisticLikes, addOptimisticLike] = useOptimistic(
likes,
(state) => state + 1
);
async function handleLike() {
addOptimisticLike();
await api.like();
setLikes(l => l + 1);
}
return <button onClick={handleLike}>👍 {optimisticLikes}</button>;
}
function Comments({ commentsPromise }) {
const comments = use(commentsPromise);
return comments.map(c => <Comment key={c.id} {...c} />);
}
function Input({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
function BlogPost({ post }) {
return (
<article>
<title>{post.title}</title> {/* 🔥 自动提升到 <head> */}
<meta name="description" content={post.excerpt} />
<h1>{post.title}</h1>
</article>
);
}
Vue 3.5 重大更新
const { count } = defineProps(['count']);
watchEffect(() => {
console.log(count);
});
const inputRef = useTemplateRef<HTMLInputElement>('input');
const id = useId();
import { defineAsyncComponent, hydrateOnVisible } from 'vue';
const HeavyComponent = defineAsyncComponent({
loader: () => import('./Heavy.vue'),
hydrate: hydrateOnVisible()
});
新特性对比
| 特性 | React 19 | Vue 3.5 |
|---|
| 异步状态 | useActionState | 无(用 composable) |
| 乐观更新 | useOptimistic | 无(手动实现) |
| Promise 消费 | use() | await(setup 中) |
| ref 传递 | 原生 prop | defineExpose |
| 文档元数据 | 原生支持 | useHead(插件) |
| 响应式优化 | - | 内存减少 56% |
| Props 解构 | - | 保持响应式 |
| 懒加载 hydration | - | hydrateOnVisible |
一、设计哲学:两种思维方式
1.1 核心公式
React: UI = f(state) → 状态的函数
Vue: UI = render(reactive(state)) → 响应式渲染
1.2 一图看懂差异
┌─────────────────────────────────────────────────────────────────┐
│ React 的世界观 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ "每次渲染都是一张新照片" │
│ │
│ state = { count: 0 } │
│ │ │
│ ▼ setState({ count: 1 }) │
│ ┌─────────────┐ │
│ │ 重新执行 │ ← 整个组件函数重新运行 │
│ │ 组件函数 │ ← 创建新的 JSX │
│ └─────────────┘ ← Diff 找出变化 │
│ │ │
│ ▼ │
│ 更新 DOM │
│ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Vue 的世界观 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ "我知道谁在用这个数据" │
│ │
│ state = reactive({ count: 0 }) │
│ │ │
│ ▼ state.count = 1 │
│ ┌─────────────┐ │
│ │ Proxy 拦截 │ ← 知道 count 变了 │
│ │ trigger() │ ← 知道谁依赖 count │
│ └─────────────┘ ← 只更新相关部分 │
│ │ │
│ ▼ │
│ 精准更新 DOM │
│ │
└─────────────────────────────────────────────────────────────────┘
1.3 核心差异表
| 维度 | React | Vue |
|---|
| 数据理念 | 不可变(Immutable) | 可变(Mutable) |
| 更新触发 | 显式调用 setState | 自动追踪依赖 |
| 更新粒度 | 组件级别 | 属性级别 |
| 模板方案 | JSX(JS 中写 HTML) | Template(HTML 中写 JS) |
| 优化方式 | 手动(useMemo/useCallback) | 自动(编译时优化) |
| 心智模型 | 函数式思维 | 响应式思维 |
二、架构对比:三大模块 vs 三大系统
┌─────────────────────────────────────────────────────────────────┐
│ React 架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Scheduler │ │ Reconciler │ │ Renderer │ │
│ │ 调度器 │ │ 协调器 │ │ 渲染器 │ │
│ │ │ │ │ │ │ │
│ │ • 优先级 │ │ • Fiber树 │ │ • ReactDOM │ │
│ │ • 时间切片 │ │ • Diff算法 │ │ • Native │ │
│ │ • 最小堆 │ │ • Hooks │ │ • Three.js │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │
│ └────────────────┼────────────────┘ │
│ ▼ │
│ 可中断渲染 + 优先级抢占 │
│ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Vue 架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Compiler │ │ Runtime │ │ Reactivity │ │
│ │ 编译器 │ │ 运行时 │ │ 响应式 │ │
│ │ │ │ │ │ │ │
│ │ • Parse │ │ • Renderer │ │ • Proxy │ │
│ │ • Transform │ │ • Scheduler │ │ • Dep/Effect│ │
│ │ • Codegen │ │ • Component │ │ • Computed │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │
│ └────────────────┼────────────────┘ │
│ ▼ │
│ 编译时优化 + 精准更新 │
│ │
└─────────────────────────────────────────────────────────────────┘
三、响应式原理:不可变 vs Proxy
3.1 React:不可变数据 + 调度更新
const [count, setCount] = useState(0);
setCount(1);
function dispatchSetState(fiber, queue, action) {
const update = { action, next: null };
const currentState = queue.lastRenderedState;
const eagerState = typeof action === 'function'
? action(currentState)
: action;
if (Object.is(eagerState, currentState)) {
return;
}
enqueueConcurrentHookUpdate(fiber, queue, update);
scheduleUpdateOnFiber(fiber);
}
3.2 Vue:Proxy + 依赖追踪
const state = reactive({ count: 0 });
state.count = 1;
const mutableHandlers = {
get(target, key) {
track(target, key);
return Reflect.get(target, key);
},
set(target, key, value) {
const oldValue = target[key];
const result = Reflect.set(target, key, value);
if (hasChanged(value, oldValue)) {
trigger(target, key);
}
return result;
}
};
3.3 依赖收集对比图
┌─────────────────────────────────────────────────────────────────┐
│ React:无依赖追踪 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ function Component() { │
│ const [a, setA] = useState(0); │
│ const [b, setB] = useState(0); │
│ │
│ return <div>{a} + {b}</div>; │
│ } │
│ │
│ setA(1); → 整个组件重新执行 → 重新计算 a + b │
│ setB(1); → 整个组件重新执行 → 重新计算 a + b │
│ │
│ 💡 React 不知道 div 只依赖 a 和 b,所以每次都全量执行 │
│ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Vue:精准依赖追踪 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ const a = ref(0); │
│ const b = ref(0); │
│ │
│ // 渲染时自动收集依赖 │
│ effect(() => { │
│ render(a.value + b.value); │
│ }); │
│ // Vue 知道:这个 effect 依赖 a 和 b │
│ │
│ a.value = 1; → 只触发依赖 a 的 effect │
│ b.value = 1; → 只触发依赖 b 的 effect │
│ │
│ 💡 Vue 精确知道谁依赖谁,只更新必要的部分 │
│ │
└─────────────────────────────────────────────────────────────────┘
3.4 响应式对比表
| 特性 | React | Vue |
|---|
| 实现方式 | 不可变数据 + setState | Proxy + 依赖追踪 |
| 更新粒度 | 组件级别 | 属性级别 |
| 深层响应 | 需要展开/immer | 自动深层响应 |
| 数组更新 | 需要新数组 | 直接 push/splice |
| 性能优化 | 手动 useMemo/useCallback | 自动,computed 缓存 |
| 心智负担 | 需要理解闭包、依赖数组 | 需要理解 .value |
四、Hooks vs Composition API:殊途同归
4.1 代码对比
function useCounter() {
const [count, setCount] = useState(0);
const double = useMemo(() => count * 2, [count]);
useEffect(() => {
document.title = `Count: ${count}`;
return () => console.log('cleanup');
}, [count]);
return { count, double, increment: () => setCount(c => c + 1) };
}
function useCounter() {
const count = ref(0);
const double = computed(() => count.value * 2);
watchEffect((onCleanup) => {
document.title = `Count: ${count.value}`;
onCleanup(() => console.log('cleanup'));
});
return { count, double, increment: () => count.value++ };
}
4.2 useEffect vs watch/watchEffect
┌─────────────────────────────────────────────────────────────────┐
│ useEffect 执行时机 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Render 阶段 ──► Commit 阶段 ──► 浏览器绘制 ──► useEffect │
│ │ │ │ │ │
│ │ │ │ ▼ │
│ 执行组件函数 DOM 操作 屏幕更新 异步执行回调 │
│ │
│ 💡 useEffect 在浏览器绘制后异步执行,不阻塞渲染 │
│ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ watchEffect 执行时机 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 数据变化 ──► queueJob ──► flushJobs ──► watchEffect ──► DOM │
│ │ │ │ │ │ │
│ │ │ │ ▼ │ │
│ trigger() 入队更新 微任务执行 同步执行回调 patch │
│ │
│ 💡 watchEffect 默认在 DOM 更新前同步执行 │
│ 💡 可通过 flush: 'post' 改为 DOM 更新后执行 │
│ │
└─────────────────────────────────────────────────────────────────┘
4.3 副作用对比表
| 特性 | useEffect | watchEffect | watch |
|---|
| 依赖追踪 | 手动声明数组 | 自动追踪 | 显式指定 |
| 执行时机 | 异步(绘制后) | 同步(更新前) | 可配置 |
| 获取旧值 | 需要 useRef | ❌ | ✅ 回调参数 |
| 清理函数 | return 返回 | onCleanup 参数 | onCleanup |
| 立即执行 | 首次渲染后 | 立即执行 | immediate 选项 |
| 闭包陷阱 | ⚠️ 存在 | ✅ 不存在 | ✅ 不存在 |
4.4 闭包陷阱详解
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
console.log(count);
}, 1000);
return () => clearInterval(timer);
}, []);
}
const count = ref(0);
watchEffect(() => {
setInterval(() => {
console.log(count.value);
}, 1000);
});
五、虚拟 DOM:Fiber 链表 vs VNode 数组
5.1 数据结构对比
interface FiberNode {
tag: number;
type: any;
key: string | null;
return: Fiber | null;
child: Fiber | null;
sibling: Fiber | null;
memoizedState: any;
memoizedProps: any;
flags: number;
subtreeFlags: number;
alternate: Fiber | null;
}
interface VNode {
type: VNodeTypes;
props: VNodeProps | null;
key: string | number | null;
children: VNodeChildren;
el: Element | null;
shapeFlag: number;
patchFlag: number;
dynamicProps: string[];
dynamicChildren: VNode[];
}
5.2 结构可视化
┌─────────────────────────────────────────────────────────────────┐
│ React Fiber 树(链表) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ App │
│ │ │
│ child │
│ ▼ │
│ Header ──sibling──► Main ──sibling──► Footer
│ │ │ │
│ child child │
│ ▼ ▼ │
│ Logo Content │
│ │
│ 遍历顺序:App → Header → Logo → Main → Content → Footer │
│ 💡 链表结构可以随时中断,记住位置后继续 │
│ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Vue VNode 树(数组) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ App │
│ │ │
│ children │
│ ▼ │
│ [Header, Main, Footer] │
│ │ │ │ │
│ children children children │
│ ▼ ▼ ▼ │
│ [Logo] [Content] [] │
│ │
│ 💡 数组结构更直观,但不支持中断 │
│ 💡 通过 dynamicChildren 只追踪动态节点 │
│ │
└─────────────────────────────────────────────────────────────────┘
5.3 虚拟 DOM 对比表
| 特性 | React Fiber | Vue VNode |
|---|
| 树结构 | 链表(child/sibling/return) | 数组(children) |
| 可中断 | ✅ 链表便于中断恢复 | ❌ 同步渲染 |
| 双缓冲 | ✅ current/workInProgress | ❌ 直接复用 |
| 编译优化 | ❌ 纯运行时 | ✅ PatchFlags/Block Tree |
| 静态提升 | ❌ | ✅ 编译时提升到函数外 |
| 内存占用 | 较大(双树 + 链表指针) | 较小 |
六、Diff 算法:三轮遍历 vs 双端 + 最长递增子序列
6.1 算法流程对比
┌─────────────────────────────────────────────────────────────────┐
│ React Diff(三轮遍历) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 旧: [A, B, C, D, E] │
│ 新: [A, C, D, B, F] │
│ │
│ 第一轮:从左到右,处理更新 │
│ ───────────────────────── │
│ A === A ✓ patch │
│ B !== C ✗ 跳出 │
│ │
│ 第二轮:处理新增/删除 │
│ ───────────────────────── │
│ 旧节点没遍历完,新节点没遍历完 → 进入第三轮 │
│ │
│ 第三轮:处理移动(Map + lastPlacedIndex) │
│ ───────────────────────── │
│ 建立 Map: { B:1, C:2, D:3, E:4 } │
│ 遍历新节点 [C, D, B, F]: │
│ - C: oldIndex=2, lastPlacedIndex=0 → 不移动, lastPlacedIndex=2│
│ - D: oldIndex=3 > 2 → 不移动, lastPlacedIndex=3 │
│ - B: oldIndex=1 < 3 → 🔥 需要移动 │
│ - F: 不在 Map → 新增 │
│ - E: 在 Map 但不在新列表 → 删除 │
│ │
│ 结果:移动 B,新增 F,删除 E │
│ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Vue Diff(双端 + LIS) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 旧: [A, B, C, D, E] │
│ 新: [A, C, D, B, F] │
│ │
│ Step 1:头部同步 │
│ ───────────────────────── │
│ A === A ✓ patch, i++ │
│ │
│ Step 2:尾部同步 │
│ ───────────────────────── │
│ E !== F ✗ 停止 │
│ │
│ Step 3:中间乱序部分 │
│ ───────────────────────── │
│ 旧: [B, C, D, E] 新: [C, D, B, F] │
│ │
│ 建立 keyToNewIndexMap: { C:0, D:1, B:2, F:3 } │
│ newIndexToOldIndexMap: [2, 3, 1, 0] (C在旧2, D在旧3, B在旧1) │
│ │
│ 🔥 最长递增子序列: [2, 3] → 索引 [0, 1] │
│ - C (索引0) 在 LIS 中 → 不移动 │
│ - D (索引1) 在 LIS 中 → 不移动 │
│ - B (索引2) 不在 LIS → 需要移动 │
│ - F (索引3) oldIndex=0 → 新增 │
│ - E 不在新列表 → 删除 │
│ │
│ 结果:移动 B,新增 F,删除 E(与 React 相同,但移动更少) │
│ │
└─────────────────────────────────────────────────────────────────┘
6.2 移动次数对比示例
旧: [A, B, C, D]
新: [A, D, B, C]
┌─────────────────────────────────────────────────────────────────┐
│ React(lastPlacedIndex) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ A: oldIndex=0, lastPlacedIndex=0 → 不移动 │
│ D: oldIndex=3, lastPlacedIndex=3 → 不移动 │
│ B: oldIndex=1 < 3 → 🔥 移动 │
│ C: oldIndex=2 < 3 → 🔥 移动 │
│ │
│ 结果:移动 2 次(B 和 C) │
│ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Vue(最长递增子序列) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ newIndexToOldIndexMap: [1, 4, 2, 3] (A=1, D=4, B=2, C=3) │
│ 最长递增子序列: [1, 2, 3] → 索引 [0, 2, 3] │
│ │
│ A (索引0) 在 LIS → 不移动 │
│ D (索引1) 不在 LIS → 🔥 移动 │
│ B (索引2) 在 LIS → 不移动 │
│ C (索引3) 在 LIS → 不移动 │
│ │
│ 结果:移动 1 次(只移动 D) │
│ │
└─────────────────────────────────────────────────────────────────┘
6.3 Diff 算法对比表
| 特性 | React | Vue |
|---|
| 算法 | 三轮遍历 + Map | 双端对比 + 最长递增子序列 |
| 移动判断 | lastPlacedIndex | 最长递增子序列 |
| 时间复杂度 | O(n) | O(n log n)(LIS 用二分) |
| 移动次数 | 可能较多 | 理论最少 |
| 实现复杂度 | 简单 | 较复杂 |
七、调度机制:时间切片 vs 微任务批量
7.1 调度策略对比
┌─────────────────────────────────────────────────────────────────┐
│ React Scheduler(时间切片) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 优先级(5 个级别): │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ ImmediatePriority │ -1ms │ 立即执行(同步) │ │
│ │ UserBlockingPriority│ 250ms │ 用户交互(点击、输入) │ │
│ │ NormalPriority │ 5000ms │ 普通更新(setState) │ │
│ │ LowPriority │ 10000ms │ 低优先级 │ │
│ │ IdlePriority │ ∞ │ 空闲执行 │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ 时间切片(5ms 一片): │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ 5ms │→│ 5ms │→│ 5ms │→│ 5ms │→ ... │
│ │work │ │work │ │work │ │work │ │
│ └─────┘ └─────┘ └─────┘ └─────┘ │
│ ↓ ↓ ↓ ↓ │
│ yield yield yield yield ← 让出主线程 │
│ │
│ 💡 高优先级可以打断低优先级 │
│ 💡 用户交互不会被长任务阻塞 │
│ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Vue Scheduler(微任务批量) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 同步代码执行: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ state.a = 1 → trigger → queueJob(update) │ │
│ │ state.b = 2 → trigger → 已入队,跳过 │ │
│ │ state.c = 3 → trigger → 已入队,跳过 │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 微任务执行:Promise.resolve().then(flushJobs) │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ 1. 按组件 id 排序(父组件先于子组件) │ │
│ │ 2. 依次执行 update 任务 │ │
│ │ 3. 执行 postFlushCbs(mounted/updated) │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ 💡 多次修改只触发一次更新 │
│ 💡 简单高效,但不支持中断 │
│ │
└─────────────────────────────────────────────────────────────────┘
7.2 核心代码对比
function workLoop() {
while (task !== null && !shouldYield()) {
task = performTask(task);
}
if (task !== null) {
scheduleCallback(task);
}
}
function shouldYield() {
return getCurrentTime() - startTime > 5;
}
const channel = new MessageChannel();
channel.port1.onmessage = performWorkUntilDeadline;
function queueJob(job) {
if (!queue.includes(job)) {
queue.splice(findInsertionIndex(job.id), 0, job);
queueFlush();
}
}
function queueFlush() {
if (!currentFlushPromise) {
currentFlushPromise = Promise.resolve().then(flushJobs);
}
}
function flushJobs() {
for (const job of queue) {
job();
}
queue.length = 0;
flushPostFlushCbs();
}
7.3 调度对比表
| 特性 | React | Vue |
|---|
| 调度策略 | 优先级 + 时间切片 | 微任务批量更新 |
| 可中断 | ✅ Concurrent Mode | ❌ 同步渲染 |
| 优先级 | 5 个级别 | 无(按组件 id 排序) |
| 时间切片 | ✅ 5ms 一片 | ❌ |
| 实现方式 | MessageChannel | Promise.resolve() |
| 任务队列 | 最小堆 | 数组(二分插入) |
| 复杂度 | 高 | 低 |
八、编译优化:运行时 vs 编译时
8.1 编译对比
<div className="container">
<span>{msg}</span>
<p>static text</p>
</div>
React.createElement("div", { className: "container" },
React.createElement("span", null, msg),
React.createElement("p", null, "static text")
);
<template>
<div class="container">
<span>{{ msg }}</span>
<p>static text</p>
</div>
</template>
const _hoisted_1 = _createElementVNode("p", null, "static text", -1)
function render(_ctx) {
return (_openBlock(), _createElementBlock("div", { class: "container" }, [
_createElementVNode("span", null, _toDisplayString(_ctx.msg), 1 ),
_hoisted_1
]))
}
8.2 Block Tree 优化
┌─────────────────────────────────────────────────────────────────┐
│ Vue Block Tree │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 传统 Diff:遍历整棵树 │
│ ┌─────────────────────────────────────────┐ │
│ │ div │ │
│ │ / | \ │ │
│ │ span p ul │ ← 需要遍历所有 │
│ │ | / | \ │ │
│ │ {msg} li li li │ │
│ └─────────────────────────────────────────┘ │
│ │
│ Block Tree:只追踪动态节点 │
│ ┌─────────────────────────────────────────┐ │
│ │ div (Block) │ │
│ │ │ │ │
│ │ dynamicChildren: [ │ ← 只遍历动态 ││ │ span (TEXT), │ ││ │ ] │ │
│ └─────────────────────────────────────────┘ │
│ │
│ 💡 静态节点完全跳过,只 patch 动态节点 │
│ │
└─────────────────────────────────────────────────────────────────┘
8.3 PatchFlags 详解
enum PatchFlags {
TEXT = 1,
CLASS = 2,
STYLE = 4,
PROPS = 8,
FULL_PROPS = 16,
NEED_HYDRATION = 32,
STABLE_FRAGMENT = 64,
KEYED_FRAGMENT = 128,
UNKEYED_FRAGMENT = 256,
NEED_PATCH = 512,
DYNAMIC_SLOTS = 1024,
HOISTED = -1,
BAIL = -2
}
if (patchFlag & PatchFlags.TEXT) {
hostSetElementText(el, newChildren);
}
if (patchFlag & PatchFlags.CLASS) {
hostPatchProp(el, 'class', null, newProps.class);
}
8.4 编译优化对比表
| 优化 | React | Vue |
|---|
| 静态提升 | ❌ | ✅ 静态节点只创建一次 |
| PatchFlags | ❌ | ✅ 标记动态内容类型 |
| Block Tree | ❌ | ✅ 只追踪动态节点 |
| 事件缓存 | ❌ 需 useCallback | ✅ 自动缓存 |
| 预字符串化 | ❌ | ✅ 连续静态节点合并为字符串 |
| 编译时优化 | 无 | 显著减少运行时开销 |
九、组件更新流程:完整对比
9.1 React 更新流程
┌─────────────────────────────────────────────────────────────────┐
│ React 完整更新流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ setState(newValue) │
│ │ │
│ ▼ │
│ dispatchSetState() │
│ │ │
│ ├── 🔥 Eager State:提前计算,相同值跳过 │
│ │ │
│ ▼ │
│ scheduleUpdateOnFiber() ← 标记 Fiber 需要更新 │
│ │ │
│ ▼ │
│ ensureRootIsScheduled() ← 确保根节点被调度 │
│ │ │
│ ▼ │
│ Scheduler.scheduleCallback(priority, callback) │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Render 阶段(可中断) │ │
│ │ │ │
│ │ workLoopConcurrent() │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ performUnitOfWork(fiber) │ │
│ │ │ │ │
│ │ ├── beginWork() ← 递:执行组件函数,处理 Hooks │ │
│ │ │ │ │
│ │ └── completeWork() ← 归:创建 DOM,收集副作用 │ │
│ │ │ │
│ │ 💡 每处理一个 Fiber 检查 shouldYield() │ │
│ │ 💡 超时则让出主线程,下次继续 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Commit 阶段(不可中断) │ │
│ │ │ │
│ │ Before Mutation ← getSnapshotBeforeUpdate │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Mutation ← DOM 操作(增删改) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 🔥 root.current = finishedWork ← 切换 Fiber 树 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Layout ← useLayoutEffect、componentDidMount │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ flushPassiveEffects() ← useEffect(异步,浏览器绘制后) │
│ │
└─────────────────────────────────────────────────────────────────┘
9.2 Vue 更新流程
┌─────────────────────────────────────────────────────────────────┐
│ Vue 完整更新流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ state.count = newValue │
│ │ │
│ ▼ │
│ Proxy set 拦截 │
│ │ │
│ ▼ │
│ trigger(target, key) ← 触发依赖 │
│ │ │
│ ▼ │
│ 遍历 Dep.subs,调用 effect.scheduler() │
│ │ │
│ ▼ │
│ queueJob(componentUpdate) ← 入队更新任务 │
│ │ │
│ ├── 🔥 去重:相同任务不重复入队 │
│ ├── 🔥 排序:按组件 id 排序(父先于子) │
│ │ │
│ ▼ │
│ queueFlush() ← Promise.resolve().then(flushJobs) │
│ │ │
│ ▼ │
│ ─────────── 同步代码结束,微任务开始 ─────────── │
│ │ │
│ ▼ │
│ flushJobs() │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 组件更新 │ │
│ │ │ │
│ │ componentUpdateFn() │ │
│ │ │ │ │
│ │ ├── onBeforeUpdate hooks │ │
│ │ │ │ │
│ │ ├── render() ← 生成新 VNode │ │
│ │ │ │ │
│ │ ├── patch(prevTree, nextTree) ← Diff + DOM 更新 │ │
│ │ │ │ │ │
│ │ │ ├── 🔥 Block Tree:只遍历动态节点 │ │
│ │ │ └── 🔥 PatchFlags:精准更新 │ │
│ │ │ │ │
│ │ └── queuePostFlushCb(onUpdated) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ flushPostFlushCbs() ← onMounted/onUpdated hooks │
│ │
└─────────────────────────────────────────────────────────────────┘
9.3 更新流程对比表
| 阶段 | React | Vue |
|---|
| 触发更新 | setState → dispatchSetState | Proxy set → trigger |
| 调度方式 | Scheduler + 优先级 | 微任务 + 去重排序 |
| 渲染阶段 | Render(可中断) | 同步执行 |
| DOM 操作 | Commit(不可中断) | patch(同步) |
| 副作用执行 | useEffect(异步) | watchEffect(可配置) |
| 生命周期 | Layout 阶段执行 | postFlushCbs 执行 |
十、生命周期对比
┌─────────────────────────────────────────────────────────────────┐
│ 生命周期映射 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ React Class React Hooks Vue 3 │
│ ─────────── ─────────── ───── │
│ constructor - setup() │
│ render return JSX render()/template │
│ componentDidMount useEffect(,[]) onMounted │
│ componentDidUpdate useEffect onUpdated │
│ componentWillUnmount useEffect return onBeforeUnmount │
│ - - onUnmounted │
│ - useLayoutEffect onBeforeMount+Mounted│
│ getDerivedState - watch + computed │
│ shouldComponentUpdate React.memo -(自动优化) │
│ getSnapshotBefore - onBeforeUpdate │
│ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 执行时机对比 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ React: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Render → Commit(Layout) → 浏览器绘制 → useEffect │ │
│ │ ↑ ↑ │ │
│ │ useLayoutEffect 异步执行 │ │
│ │ componentDidMount │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Vue: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ setup → beforeMount → patch → mounted → 浏览器绘制 │ │
│ │ ↑ ↑ │ │
│ │ DOM更新 postFlushCbs │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
十一、性能对比
11.1 Benchmark 实测数据
┌─────────────────────────────────────────────────────────────────┐
│ js-framework-benchmark 实测 │
│ (Chrome 120, 越低越好,单位 ms) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 创建 1000 行 │
│ React 19: ████████████████████ 45.2ms │
│ Vue 3.5: ████████████████ 38.6ms │
│ │
│ 更新 1000 行(每行修改) │
│ React 19: ████████████████████████ 52.8ms │
│ Vue 3.5: ██████████████ 31.4ms ← 🔥 快 40% │
│ │
│ 部分更新(每 10 行修改 1 行) │
│ React 19: ████████████████████ 44.1ms │
│ Vue 3.5: ████████ 18.2ms ← 🔥 快 59% │
│ │
│ 交换两行 │
│ React 19: ████████████████ 35.6ms │
│ Vue 3.5: ████████████ 28.3ms │
│ │
│ 删除一行 │
│ React 19: ████████████ 26.4ms │
│ Vue 3.5: ██████████ 22.1ms │
│ │
│ 内存占用(创建 1000 行后) │
│ React 19: ████████████████████ 8.2MB │
│ Vue 3.5: ████████████ 5.1MB ← 🔥 省 38% │
│ │
└─────────────────────────────────────────────────────────────────┘
💡 Vue 在部分更新场景优势明显(编译优化 + 精准更新)
💡 React 在复杂交互场景更稳定(时间切片避免卡顿)
11.2 性能差异根因分析
┌─────────────────────────────────────────────────────────────────┐
│ 为什么 Vue 部分更新更快? │
├─────────────────────────────────────────────────────────────────┤
│ │
│ React 更新 1 个 item: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. setState 触发 │ │
│ │ 2. 整个 List 组件重新执行 │ │
│ │ 3. 创建 1000 个新的 JSX 对象 │ │
│ │ 4. Diff 1000 个节点,找出 1 个变化 │ │
│ │ 5. 更新 1 个 DOM │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Vue 更新 1 个 item: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. Proxy set 触发 │ │
│ │ 2. 找到依赖这个数据的 1 个组件 │ │
│ │ 3. 只重新渲染这 1 个 Item 组件 │ │
│ │ 4. Block Tree:只 Diff 动态节点 │ │
│ │ 5. 更新 1 个 DOM │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 💡 Vue 的优势:精准定位 + 最小化渲染范围 │
│ │
└─────────────────────────────────────────────────────────────────┘
11.3 性能对比表
| 维度 | React | Vue | 说明 |
|---|
| 首次渲染 | 较快 | 较快 | 差异不大 |
| 更新性能 | 组件级 Diff | 精准更新 | Vue 更新粒度更细 |
| 大列表 | 需要虚拟滚动 | 需要虚拟滚动 | 都需要优化 |
| 内存占用 | Fiber 双树开销 | VNode 开销较小 | Vue 3.5 优化后更明显 |
| 包体积 | ~42KB | ~33KB | Vue 更小 |
| 编译优化 | 无 | 显著 | Vue 编译时优化明显 |
| 手动优化 | useMemo/useCallback | 基本不需要 | React 需要更多手动优化 |
十二、SSR/SSG 对比:服务端渲染的差异
12.1 架构对比
┌─────────────────────────────────────────────────────────────────┐
│ React SSR 架构(Next.js) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Server Components │ │
│ │ │ │
│ │ • 默认在服务端执行,不发送 JS 到客户端 │ │
│ │ • 可以直接访问数据库、文件系统 │ │
│ │ • 不能使用 useState、useEffect │ │
│ │ │ │
│ │ async function Page() { │ │
│ │ const data = await db.query('SELECT * FROM posts'); │ │
│ │ return <PostList posts={data} />; │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Client Components │ │
│ │ │ │
│ │ 'use client';
│ │ │ │
│ │ function LikeButton() { │ │
│ │ const [liked, setLiked] = useState(false); │ │
│ │ return <button onClick={() => setLiked(true)}> │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 💡 React 的 RSC 是革命性的:组件级别的服务端/客户端分离 │
│ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Vue SSR 架构(Nuxt 3) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Universal Components │ │
│ │ │ │
│ │ • 同一份代码,服务端和客户端都执行 │ │
│ │ • 通过 useAsyncData/useFetch 获取数据 │ │
│ │ • 自动处理 hydration │ │
│ │ │ │
│ │ <script setup> │ │
│ │ const { data } = await useFetch('/api/posts'); │ │
│ │ </script> │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Client Only │ │
│ │ │ │
│ │ <ClientOnly> │ │
│ │ <BrowserOnlyComponent /> │ │
│ │ </ClientOnly> │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 💡 Vue 的 SSR 更传统:同构渲染,代码复用率高 │
│ │
└─────────────────────────────────────────────────────────────────┘
12.2 数据获取对比
async function PostPage({ params }) {
const post = await db.post.findUnique({ where: { id: params.id } });
return <Post data={post} />;
}
'use client';
function Comments({ postId }) {
const [comments, setComments] = useState([]);
useEffect(() => {
fetch(`/api/comments/${postId}`).then(r => r.json()).then(setComments);
}, [postId]);
return <CommentList comments={comments} />;
}
<script setup>
const { data: post } = await useFetch(`/api/posts/${route.params.id}`);
const { data: comments } = await useFetch(`/api/comments`, {
server: false
});
</script>
12.3 SSR 对比表
| 特性 | React (Next.js) | Vue (Nuxt 3) |
|---|
| 渲染模式 | RSC + Client Components | Universal Components |
| 数据获取 | async 组件 / use() | useFetch / useAsyncData |
| 代码分割 | 自动(Server/Client) | 手动(ClientOnly) |
| Streaming | ✅ 原生支持 | ✅ 支持 |
| 部分 Hydration | ✅ RSC 天然支持 | ✅ Lazy Hydration |
| 学习曲线 | 较陡(RSC 概念新) | 较平缓 |
| 心智模型 | 需要区分 Server/Client | 同构思维 |
十三、状态管理对比:Redux vs Pinia
13.1 设计哲学
┌─────────────────────────────────────────────────────────────────┐
│ Redux:单向数据流 + 不可变 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ dispatch ┌─────────┐ new state │
│ │ View │ ─────────────► │ Reducer │ ─────────────► │
│ └─────────┘ └─────────┘ │
│ ▲ │ │
│ │ │ │
│ └──────────────────────────┘ │
│ subscribe │
│ │
│
│ function reducer(state, action) { │
│ switch (action.type) { │
│ case 'increment': │
│ return { ...state, count: state.count + 1 }; │
│ } │
│ } │
│ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Pinia:响应式 + 直接修改 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ 直接修改 ┌─────────┐ 自动更新 │
│ │ View │ ─────────────► │ Store │ ─────────────► │
│ └─────────┘ └─────────┘ │
│ ▲ │ │
│ │ │ │
│ └──────────────────────────┘ │
│ 响应式追踪 │
│ │
│
│ const store = useCounterStore(); │
│ store.count++;
│ │
└─────────────────────────────────────────────────────────────────┘
13.2 代码对比
import { createSlice, configureStore } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { count: 0, loading: false },
reducers: {
increment: (state) => { state.count += 1 },
decrement: (state) => { state.count -= 1 },
},
extraReducers: (builder) => {
builder
.addCase(fetchCount.pending, (state) => { state.loading = true })
.addCase(fetchCount.fulfilled, (state, action) => {
state.count = action.payload;
state.loading = false;
});
},
});
const fetchCount = createAsyncThunk('counter/fetch', async () => {
const response = await api.getCount();
return response.data;
});
function Counter() {
const count = useSelector(state => state.counter.count);
const dispatch = useDispatch();
return (
<button onClick={() => dispatch(increment())}>
{count}
</button>
);
}
export const useCounterStore = defineStore('counter', () => {
const count = ref(0);
const loading = ref(false);
const doubleCount = computed(() => count.value * 2);
function increment() {
count.value++;
}
async function fetchCount() {
loading.value = true;
count.value = await api.getCount();
loading.value = false;
}
return { count, loading, doubleCount, increment, fetchCount };
});
<script setup>
const store = useCounterStore();
</script>
<template>
<button @click="store.increment()">{{ store.count }}</button>
</template>
13.3 状态管理对比表
| 特性 | Redux (Toolkit) | Pinia |
|---|
| 设计理念 | 单向数据流、不可变 | 响应式、可变 |
| 样板代码 | 较多(slice/reducer) | 很少 |
| TypeScript | 需要额外配置 | 原生支持,自动推断 |
| 异步处理 | createAsyncThunk | 直接 async/await |
| DevTools | ✅ 时间旅行 | ✅ 时间旅行 |
| 模块化 | 需要 combineReducers | 天然模块化 |
| 学习曲线 | 较陡 | 平缓 |
| 包体积 | ~11KB | ~1.5KB |
十四、适用场景
选择 React
- ✅ 大型复杂应用,需要精细控制
- ✅ 团队熟悉函数式编程
- ✅ 需要 React Native 跨端
- ✅ 需要丰富的生态(Next.js、Remix)
- ✅ 需要 Server Components(RSC)
- ✅ 需要 Concurrent Mode 特性(时间切片、Suspense)
- ✅ 需要更灵活的架构设计
选择 Vue
- ✅ 中小型应用,快速开发
- ✅ 团队偏好模板语法
- ✅ 需要更低的学习曲线
- ✅ 需要更好的开箱即用体验
- ✅ 需要更小的包体积和内存占用
- ✅ 需要更少的手动优化
- ✅ 国内团队(中文文档完善)
十五、总结
| 维度 | React | Vue | 胜出 |
|---|
| 响应式 | 不可变 + setState | Proxy + 依赖追踪 | Vue 更自动 |
| 副作用 | useEffect 手动依赖 | watch 自动追踪 | Vue 更智能 |
| 虚拟 DOM | Fiber 链表 | VNode 数组 | React 可中断 |
| Diff 算法 | 三轮遍历 | 双端 + LIS | Vue 移动更少 |
| 调度 | 优先级 + 时间切片 | 微任务批量 | React 更强大 |
| 编译优化 | 无 | PatchFlags/Block | Vue 更高效 |
| SSR | RSC 革命性 | 同构成熟 | 各有优势 |
| 状态管理 | Redux 严谨 | Pinia 简洁 | Pinia 更易用 |
| 性能 | 稳定 | 部分更新更快 | Vue 3.5 优势明显 |
| 心智模型 | 函数式 | 响应式 | 各有所好 |
| 生态 | 更丰富 | 更统一 | 各有优势 |
| 学习曲线 | 较陡 | 较平缓 | Vue 更友好 |
核心差异一句话总结
- React 19:拥抱不可变性,Server Components 革命,通过 Fiber 实现可中断渲染,运行时 Diff,需要手动优化
- Vue 3.5:拥抱响应式,编译时优化减少 56% 内存,精准更新,自动优化
我的建议
┌─────────────────────────────────────────────────────────────────┐
│ 2025 年选型建议 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 选 React 如果: │
│ • 需要 React Native 做移动端 │
│ • 团队有 React 经验,项目已用 React │
│ • 需要 Server Components 的革命性架构 │
│ • 大型应用,需要精细的性能调优 │
│ │
│ 选 Vue 如果: │
│ • 新项目,追求开发效率 │
│ • 团队规模不大,需要快速上手 │
│ • 对包体积和内存敏感 │
│ • 不想花太多精力在性能优化上 │
│ │
│ 💡 两个框架都是顶级选择,关键是匹配团队和项目 │
│ │
└─────────────────────────────────────────────────────────────────┘
📦 源码参考:
📚 相关专栏:
如果这篇文章对你有帮助,欢迎点赞收藏!有问题评论区见 🎉