- Vue : 基于可变数据 (Mutable) + 自动依赖追踪 (响应式系统)。
- React : 基于不可变数据 (Immutable) + 显式依赖数组 (手动触发更新)。
1. 核心概念映射表 (Cheat Sheet)
| 功能 | Vue 3 (Composition API) | React (Hooks) | 关键差异点 |
| :--- | :--- | :--- | : |
| 状态定义 | const count = ref(0) | const [count, setCount] = useState(0) | Vue 直接修改 count.value;React 必须调用 setCount |
| 计算属性 | const double = computed(() => count.value * 2) | const double = useMemo(() => count * 2, [count]) | React 需手动指定依赖 [count] |
| 副作用 | watchEffect(() => { ... })
watch(source, cb) | useEffect(() => { ... }, [deps]) | React 的 useEffect 同时涵盖 mount/update/unmount |
| 清理副作用 | onUnmounted(() => ...)
(在 watch 中 return) | useEffect(() => { return () => ... }, []) | 清理函数在 return 中返回 |
| 模板/JSX | <template>{``{ count }}</template> | return <div>{count}</div> | React 直接用 JS 表达式,无插值符号 {``{ }} |
| 条件渲染 | v-if="isOk" | { isOk && <div>...</div> } | React 使用逻辑与运算符或三元表达式 |
| 列表渲染 | v-for="item in list" :key="item.id" | {list.map(item => <div key={item.id}>)} | React 必须显式写 map 和 key |
| 事件绑定 | @click="handleClick" | onClick={handleClick} | React 用驼峰 onClick,传函数引用 (不调用) |
| 双向绑定 | v-model="text" | value={text} onChange={(e) => setText(e.target.value)} | React 没有内置指令,需手动绑定 value + onChange |
| 组件通信 | props, emit('event') | props, callback props | React 单向数据流,子传父通过回调函数 |
| 插槽 | <slot />, <slot name="header"> | props.children, props.header | React 把插槽当作 props 传递 (通常是 children) |
| 生命周期 | onMounted, onUpdated | useEffect(..., []) (Mount)
useEffect(..., [deps]) (Update) | React 将生命周期合并进 useEffect |
| 样式绑定 | :class="{ active: isActive }" | className={isActive ? 'active' : ''} | React 用 className,逻辑需用 JS 三元或库 (clsx) |
| Refs/DOM | const el = ref(null)
<div ref="el"> | const el = useRef(null)
<div ref={el}> | Vue ref 取值 .value;React ref 取值 .current |
2. 代码实战对比 (Side-by-Side)
场景 A: 基础计数器 (State & Events)
Vue 3:
javascript
<script setup>
import { ref } from 'vue';
const count = ref(0);
const increment = () => count.value++;
</script>
<template>
<button @click="increment">Count: {{ count }}</button>
</template>
React:
javascript
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const increment = () => setCount(prev => prev + 1); // 习惯用函数式更新
return (
<button onClick={increment}>Count: {count}</button>
);
}
注意 : React 中事件处理函数不要加括号 (),除非你需要传参(此时用箭头函数包裹)。
场景 B: 异步数据获取 (Effects)
Vue 3:
javascript
<script setup>
import { ref, onMounted } from 'vue';
const data = ref(null);
const loading = ref(false);
const fetchData = async () => {
loading.value = true;
data.value = await fetch('/api').then(r => r.json());
loading.value = false;
};
onMounted(fetchData);
</script>
React:
javascript
import { useState, useEffect } from 'react';
function DataFetcher() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
let isMounted = true; // 防止组件卸载后更新状态
const fetchData = async () => {
setLoading(true);
const res = await fetch('/api');
const json = await res.json();
if (isMounted) setData(json);
setLoading(false);
};
fetchData();
// 清理函数 (对应 onUnmounted)
return () => { isMounted = false; };
}, []); // 空依赖数组 = 只在挂载时运行
return <div>{loading ? 'Loading...' : JSON.stringify(data)}</div>;
}
场景 C: 父子组件通信 (Props & Callbacks)
Vue 3:
javascript
<!-- Child.vue -->
<script setup>
defineProps(['msg']);
const emit = defineEmits(['update']);
</script>
<template>
<button @click="emit('update', 'new value')">{{ msg }}</button>
</template>
React:
javascript
// Child.jsx
function Child({ msg, onUpdate }) {
return (
<button onClick={() => onUpdate('new value')}>{msg}</button>
);
}
// Parent.jsx
<Child msg="Hello" onUpdate={(val) => console.log(val)} />
关键点 : React 没有 $emit。父组件传递一个函数作为 prop,子组件调用该函数。
场景 D: 列表与条件渲染
Vue 3:
javascript
<template>
<ul v-if="items.length">
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
</ul>
<p v-else>No items</p>
</template>
React:
javascript
return (
<>
{items.length > 0 ? (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
) : (
<p>No items</p>
)}
</>
);
技巧 : 推荐使用 clsx 或 classnames 库来处理复杂的 class 拼接,替代 Vue 的对象语法。
3. 生态库迁移指南 (Vue -> React)
| 类别 | Vue 3 生态 | React 推荐生态 (2026标准) | 备注 |
|---|---|---|---|
| 路由 | Vue Router | React Router v6/v7 或 Next.js App Router | Next.js 是首选,内置路由 |
| 状态管理 | Pinia / Vuex | Zustand (最流行), Redux Toolkit, Jotai | Zustand 最像 Pinia,简单无样板 |
| HTTP | Axios / VueUse | TanStack Query (React Query), Axios | React Query 是服务端状态管理的神器 |
| 表单 | VeeValidate / FormKit | React Hook Form, Zod (校验) | React Hook Form 性能极佳 |
| UI 组件库 | Element Plus / AntDV | Ant Design, MUI, Shadcn/ui (最火) | Shadcn/ui 是复制代码而非安装包,极度灵活 |
| 工具函数 | VueUse | Lodash, date-fns, clsx | React 没有官方工具集,社区组合使用 |
| CSS 方案 | Scoped CSS / SCSS | Tailwind CSS (主流), CSS Modules, Styled-components | Tailwind 在 React 生态中占据统治地位 |
4. 避坑指南:Vue 开发者转 React 的常见错误
- 直接修改 State ❌
- Vue:
count.value++✅ - React:
count++❌ (不会触发重渲染) - React:
setCount(count + 1)✅
- Vue:
- 在 Render 函数中执行副作用 ❌
- 不要在组件函数体直接写
fetch()或console.log('render')(除非受控),这会导致无限循环。必须放在useEffect中。
- 不要在组件函数体直接写
- 依赖数组遗漏 ⚠️
- Vue 自动追踪依赖。
- React 必须手动在
useEffect或useMemo的[]中列出所有用到的变量,否则会遇到"闭包陷阱"(拿到旧值)。
- Key 的使用 ⚠️
- Vue 有时不写 key 也能跑。
- React 必须 为
map生成的列表提供稳定的key(不要用 index,除非列表静态)。
- Ref 的取值 ⚠️
- Vue:
myRef.value - React:
myRef.current(且修改ref.current不会触发重渲染,它用于存储可变但不需要视图更新的值)。
- Vue: