一、前言
从 React 转 Vue3,相信很多前端工程师都有过这个经历。两者虽然都致力于"构建用户界面",但设计思想、API 风格、状态管理机制都有本质差异。本文专门针对 React 开发者视角,对照讲解 Vue3 的核心概念,帮助你快速建立 Vue3 思维模型,少走弯路。
本文重点覆盖:响应式系统对比、组件写法差异、Hooks 与 Composition API 的对照、状态管理方案、以及常见思维误区和正确做法。每个知识点都配有真实可运行的代码示例。
二、响应式系统:核心差异
2.1 React 的响应式模型
React 使用不可变数据驱动视图更新。当 state 变化时,React 会触发重新渲染整棵组件树(通过 Virtual DOM diff 优化)。核心代码如下:
// React 组件:状态驱动 import { useState, useEffect } from 'react'; function UserProfile({ userId }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { fetchUser(userId).then(data => { setUser(data); // 触发重新渲染 setLoading(false); }); }, [userId]); // React 的思维:数据变 -> 重新渲染 -> 对比虚拟 DOM -> 最小化更新 return loading ? <Spinner /> : <div>{user.name}</div>; }关键点:React 开发者习惯"数据在前,视图在后"——先有 state,再推导 UI。状态变化后 React 会自动对比新旧 Virtual DOM,决定哪些真实 DOM 需要更新。
2.2 Vue3 的响应式模型
Vue3 使用Proxy 代理直接追踪数据变化,数据在哪被用到,Vue3 就知道要更新哪里,不需要手动触发。
// Vue3 组件:声明式模板 + 响应式数据 import { ref, computed, onMounted } from 'vue'; defineComponent({ setup() { // ref 包装响应式数据 const user = ref(null); const loading = ref(true); const userId = ref(1); // 计算属性 const displayName = computed(() => user.value?.name ?? '加载中'); onMounted(async () => { const data = await fetchUser(userId.value); user.value = data; // 直接赋值,Vue3 自动追踪 loading.value = false; }); return { user, loading, displayName }; } });<!-- Vue3 模板:直接使用响应式数据 --> <template> <div>{{ loading ? '加载中' : displayName }}</div> </template>2.3 两者的核心差异对比
| 维度 | React | Vue3 |
|---|---|---|
| 响应式实现 | 不可变 state + Virtual DOM diff | Proxy 直接代理 + 依赖追踪 |
| 更新触发 | 手动 setState / useState | 赋值即更新 |
| 渲染粒度 | 组件级,需要 React.memo 优化 | 精确到响应式依赖的 DOM 节点 |
| 学习曲线 | 思维简单,但性能优化要主动 | 上手容易,性能优化由框架兜底 |
三、组件写法对照
3.1 Props 传递与类型检查
// React Props 定义 interface UserCardProps { name: string; age: number; avatar?: string; onUpdate: (id: number, name: string) => void; } function UserCard({ name, age, avatar, onUpdate }: UserCardProps) { return <div onClick={() => onUpdate(1, name)}>{name}, {age}</div>; }// Vue3 Props 定义(defineProps 配合 TS) interface UserCardProps { name: string; age: number; avatar?: string; emit: (event: 'update', id: number, name: string) => void; } const props = defineProps<UserCardProps>(); const emit = defineEmits<{ update: [id: number, name: string]; }>(); // Vue3 使用 emit const handleClick = () => emit('update', 1, props.name);3.2 生命周期对比
// React useEffect 替代所有生命周期 useEffect(() => { // mounted console.log('组件挂载'); return () => { // unmounted cleanup console.log('组件卸载清理'); }; }, []); // 空依赖 = didMount + willUnmount useEffect(() => { // didUpdate — 当 userId 变化时执行 fetchUser(userId); }, [userId]);// Vue3 组合式 API 生命周期钩子 import { onMounted, onUnmounted, watch } from 'vue'; onMounted(() => { console.log('setup 执行 = React 的 constructor'); }); watch(userId, (newId) => { fetchUser(newId); // 等价于 React 的 useEffect(() => {...}, [userId]) }); onUnmounted(() => { console.log('组件卸载 = React 的 componentWillUnmount'); });四、Hooks vs Composition API:逐个对照
这是两者最大的差异所在,也是 React 开发者迁移 Vue3 时最需要调整思维的地方。
4.1 useState → ref / reactive
// React const [count, setCount] = useState(0); const [user, setUser] = useState({ name: '', age: 0 }); setCount(count + 1); setUser({ ...user, name: 'new name' });// Vue3 const count = ref(0); // 基础类型用 ref const user = reactive({ name: '', age: 0 }); // 对象/数组用 reactive count.value++; // ref 要 .value user.name = 'new name'; // reactive 直接改4.2 useEffect → watch / watchEffect
// React:副作用在 useEffect 里 useEffect(() => { document.title = `${user.name} 的主页`; }, [user.name]);// Vue3:watch 精确监听 watch(() => user.name, (newName) => { document.title = `${newName} 的主页`; }); // watchEffect 自动收集依赖(类似 useEffect,但更直接) watchEffect(() => { document.title = `${user.name} 的主页`; });4.3 useMemo / useCallback → computed / readonly
// React const sortedList = useMemo(() => list.filter(x => x.active).sort((a, b) => a.name.localeCompare(b.name)), [list] ); const handleSubmit = useCallback((data) => submit(data), [submit]);// Vue3 const sortedList = computed(() => list.filter(x => x.active).sort((a, b) => a.name.localeCompare(b.name)) ); const handleSubmit = (data: FormData) => submit(data); // Vue3 不需要 useCallback4.4 自定义 Hooks → Composables
// React 自定义 Hook function useUserSearch(keyword) { const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); useEffect(() => { setLoading(true); search(keyword).then(data => { setResults(data); setLoading(false); }); }, [keyword]); return { results, loading }; }// Vue3 Composables(自定义组合式函数) function useUserSearch(keyword: Ref<string>) { const results = ref([]); const loading = ref(false); watch(keyword, async (kw) => { if (!kw) { results.value = []; return; } loading.value = true; results.value = await search(kw); loading.value = false; }, { immediate: true }); return { results: readonly(results), loading: readonly(loading) }; }五、常见思维误区
误区一:用 React 的不可变思维操作 Vue3 响应式数据
React 开发者习惯了setState({ ...state, key: newVal })(展开合并),在 Vue3 中对reactive对象这样做会丢失响应式:
// 错误 ❌ — 展开后变成普通对象,失去响应式 user.value = { ...user.value, name: 'new name' }; // 正确 ✅ — 直接修改属性 user.value.name = 'new name'; // 或者用 Object.assign(对 ref) Object.assign(user.value, { name: 'new name' });误区二:把 useEffect 的依赖当成 watchEffect 的写法
useEffect 依赖数组是"被动触发",watchEffect 自动收集依赖但会立即执行一次(${b} 有lazy: true选项关闭)。${b} 中不要在 watchEffect 里直接修改被监听的对象,否则会导致死循环。
误区三:忘了 .value
ref包装的变量在 script 中是引用,必须用.value读写,但在<template>中不需要——Vue3 自动解包。
六、状态管理方案对比
| 场景 | React 推荐方案 | Vue3 推荐方案 |
|---|---|---|
| 组件本地状态 | useState | ref / reactive |
| 跨组件共享 | Context + useReducer / Zustand | provide/inject + Pinia |
| 服务端数据 | React Query / SWR | Pinia + REST / GraphQL |
| 全局配置 | Context | Pinia store |
Pinia vs Redux
// React Redux 风格 const userSlice = createSlice({ name: 'user', initialState: { name: '', token: '' }, reducers: { setUser: (state, action) => { state.name = action.payload.name; } } });// Vue3 Pinia — 更直观,不需要 action/reducer 分离 export const useUserStore = defineStore('user', { state: () => ({ name: '', token: '' }), actions: { setUser(data) { this.name = data.name; this.token = data.token; }, async fetchUser(id) { const data = await api.getUser(id); this.setUser(data); } } });七、总结:迁移 checklist
| 检查项 | React 思维 | Vue3 正确姿势 |
|---|---|---|
| 状态更新 | setState(obj) | ref.value = xxx 或 reactiveObj.key = xxx |
| 对象更新 | {...obj, key: val} | 直接赋值或 Object.assign |
| 副作用 | useEffect(() => {...}, [dep]) | watch(() => dep, fn) 或 watchEffect(fn) |
| 计算属性 | useMemo(fn, [dep]) | computed(fn) |
| 组件传值 | props + callbacks | props + defineEmits |
| 全局状态 | Context / Redux / Zustand | Pinia |
| 清理逻辑 | return () => {...} | onUnmounted(() => {...}) |
💬收藏本文!关注我,后续更新更多 React → Vue3 实战系列文章。
💬觉得有用?点赞+收藏+关注!后续持续更新《框架迁移避坑》系列,React↔Vue3↔Angular 全覆盖。
标签:React | Vue3 | 迁移 | 实战 | 前端