React 转 Vue3 迁移实战:从0到1
一、前言从 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 /template2.3 两者的核心差异对比维度ReactVue3响应式实现不可变 state Virtual DOM diffProxy 直接代理 依赖追踪更新触发手动 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 definePropsUserCardProps(); 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]);// Vue3watch 精确监听 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: Refstring) { 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 里直接修改被监听的对象否则会导致死循环。误区三忘了 .valueref包装的变量在 script 中是引用必须用.value读写但在template中不需要——Vue3 自动解包。六、状态管理方案对比场景React 推荐方案Vue3 推荐方案组件本地状态useStateref / reactive跨组件共享Context useReducer / Zustandprovide/inject Pinia服务端数据React Query / SWRPinia REST / GraphQL全局配置ContextPinia storePinia 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 callbacksprops defineEmits全局状态Context / Redux / ZustandPinia清理逻辑return () {...}onUnmounted(() {...})收藏本文关注我后续更新更多 React → Vue3 实战系列文章。觉得有用点赞收藏关注后续持续更新《框架迁移避坑》系列React↔Vue3↔Angular 全覆盖。标签React | Vue3 | 迁移 | 实战 | 前端