React 转 Vue 无缝迁移:跨框架的桥梁探索
目录
- 前言
- 2.1 响应式机制
- 2.2 渲染方式:JSX vs 模板
- 2.3 组件注册与组织
- 2.4 生命周期钩子
- 3.1 响应式数据流图
- 3.2 组件生命周期对比图
- 4.1 React 版 Todo List(初始化)
- 4.2 Vue 版 Todo List(迁移成果)
4.3 迁移步骤详解
- 4.3.1 将 JSX 转成 Vue 模板
- 4.3.2 状态管理:useState → ref/reactive
- 4.3.3 事件绑定:onClick → @click
- 4.3.4 Props 与事件传递
- 4.3.5 生命周期钩子替换
- 5.1 Hooks 模式到 Composition API
- 5.2 Redux / Context 到 Vuex / Pinia
- 5.3 第三方库适配(路由、请求库等)
- 常见痛点与解决方案
- 总结
前言
在前端生态中,React 与 Vue 各自拥有广泛的社区和生态体系。有时项目需求会让我们不得不进行框架迁移:例如,团队技术栈从 React 迁向 Vue,或同时维护 React 与 Vue 多套代码。本文将帮助你快速搭建一座“跨框架的桥梁”,让你能无缝地把 React 组件、思路与代码迁移到 Vue,并且不失“优雅与高效”。
本文特色:
- 从核心理念对比到实战示例,一步步拆解。
- 配有 ASCII 图解,直观理解数据流与生命周期。
- 提供完整代码示例,手把手演示如何从 React 版搬到 Vue 版。
- 涵盖进阶迁移策略,如 Hooks → Composition API,Redux → Pinia 等。
如果你已经具备 React 基础,并对 Vue 有所接触(或零基础也没关系),本文会让你快速上手,将 React 思维映射到 Vue 生态中。下面,让我们从最基础的“核心理念对比”说起。
核心理念对比
迁移的前提是要搞清楚两个框架背后的核心设计思路与 API 约定,便于一一映射。
2.1 响应式机制
分类 | React (18+) | Vue (3.x) |
---|---|---|
核心思想 | 函数式更新 + 虚拟 DOM Diff 组件通过 useState 维护局部 state,当 state 改变时,React 会触发虚拟 DOM 重新渲染并进行 diff。 | Proxy + 响应式追踪 + 虚拟 DOM Diff 使用 ref 或 reactive 创建响应式对象,访问或修改时触发依赖收集与更新。 |
数据更新方式 | 纯函数式:setState (或 Hooks useState 返回的 setter)会将新状态传给渲染函数。 | Proxy 拦截:对 ref.value 或 reactive 对象直接赋值,Vue 自动跟踪依赖并重新渲染。 |
优势 | 函数式更新带来的可预测性;Hooks 可组合性。 | 原生 Proxy 性能更优且语法简洁;Composition API 逻辑复用灵活。 |
小结:
- React 用 “函数式” 更新,Vue 用 “响应式引用/对象” 更新。
- 迁移时,只需要把
useState
状态换成 Vue 的ref
/reactive
,并把对 state 的读写改成.value
或直接访问属性即可。
2.2 渲染方式:JSX vs 模板
React(JSX):在 JavaScript 里使用类似 XML 的语法,以
className
、onClick
等属性绑定。所有逻辑都写在.jsx
(或.tsx
)文件里。function Hello({ name }) { return ( <div className="hello-container"> <h1 onClick={() => alert(`你好,${name}!`)}>Hello, {name}!</h1> </div> ); }
Vue(模板 +
<script>
):.vue
文件分为<template>
、<script setup>
、<style>
三个部分。模板语法更贴近 HTML,事件改成@click
,绑定指令用v-bind
或简写:
。<template> <div class="hello-container"> <h1 @click="sayHello">Hello, {{ name }}!</h1> </div> </template> <script setup> import { defineProps } from 'vue'; const props = defineProps({ name: String }); function sayHello() { alert(`你好,${props.name}!`); } </script>
小结:
- JSX 中一切写在 JavaScript 表达式里,模板更贴近 HTML + 插值表达式。
- 迁移时,需要把 JSX 里
{}
插值、三元表达式、事件绑定等映射到 Vue 模板语法:{{}}
、v-if/v-for
、@click
、:
。
2.3 组件注册与组织
- React:默认所有组件都需要手动
import
并通过export default
或export
导出;父组件里直接<Child someProp={value} />
。 - Vue:有两种模式——全局注册(应用启动时
app.component('MyComp', MyComp)
)与局部注册(在组件内components: { MyComp }
)。在 Vue 3 的<script setup>
下,局部组件可以直接在<template>
用到,前提在<script setup>
已经import MyComp from './MyComp.vue'
。
小结:
- React 与 Vue 都需要
import
/export
。Vue<template>
下的<component>
名字必须与import
的变量对应或在components
里注册。- 迁移时,只要把 React 的
import
语句放到 Vue 的<script setup>
,然后在<template>
里使用即可。
2.4 生命周期钩子
React Hooks | Vue Composition API | 说明 |
---|---|---|
useEffect(() => { ... }, []) | onMounted(() => { ... }) | 组件挂载后的副作用 |
useEffect(() => { return () => {...} }, []) | onUnmounted(() => { ... }) | 组件卸载时清理 |
useEffect(() => { ... }, [dep1, dep2]) | watch([dep1, dep2], ([new1, new2], [old1, old2]) => { ... }) | 监听依赖变化 |
无直接对比 | onUpdated(() => { ... }) | 每次更新后回调(React 里没有直接等价,若需可放到 effect) |
小结:
- React 通过
useEffect
的依赖数组实现不同时机的副作用。- Vue 拆成多个钩子(
onMounted
、onUnmounted
、onUpdated
),或用watch
监听具体响应式值。
概念映射图解
为了更直观感受两者在“数据流”和“生命周期”上的差异,下面用 ASCII 图示做简单对比。
3.1 响应式数据流图
【React 数据流】 【Vue 数据流】
┌────────────┐ setState ┌────────────┐
│ UI 渲染 │ <----------------------------│ useState │
│ (function) │ │ / useRef │
└──────┬─────┘漫游 diff 后更新 virtual DOM─>└──────┬─────┘
│ │
│ render() │ render() 成 template 编译
│ │
┌──────┴─────┐ ┌──────┴─────┐
│虚拟 DOM 1 │ │ 响应式对象 ├─> 自动收集依赖 & 重新渲染
└──────┬─────┘ └────────────┘
│
│ diff patch
↓
┌────────────┐
│ 真实 DOM │
└────────────┘
- React:调用
setState
→ 触发组件重新渲染(render) → 产生新的虚拟 DOM(Virtual DOM 2)→ 与上一次进行 diff → 最终 Patch 到真实 DOM。 - Vue:更新
ref.value
/reactive
后,触发响应式系统标记该依赖(Watcher),收集依赖后再次执行渲染函数编译模板,得到新的虚拟 DOM → diff → Patch。
3.2 组件生命周期对比图
React 生命周期 Vue 生命周期
┌─────────────────────┐ ┌────────────────────────┐
│ (Mounting 阶段) │ │ (onBeforeMount → onMounted) │
│ - render() │ │ - setup() │
│ - componentDidMount│ │ - onMounted │
└─────────┬───────────┘ └──────────┬─────────────────┘
│ Update 阶段 (依赖变化) │ Update 阶段 (响应式变化)
┌─────────┴───────────┐ ┌──────────┴─────────────────┐
│ render() │ │ template 编译 → render() │
│ componentDidUpdate │ │ onUpdated │
└─────────┬───────────┘ └──────────┬─────────────────┘
│ Unmount 阶段 │ Unmount 阶段
┌─────────┴───────────┐ ┌──────────┴─────────────────┐
│ componentWillUnmount│ │ onBeforeUnmount → onUnmounted │
└─────────────────────┘ └─────────────────────────────┘
- React:
componentDidMount
→ 每次 render →componentDidUpdate
→ 卸载时componentWillUnmount
。现代 Hooks 里用useEffect
模拟。 - Vue:
setup
里初始化所有响应式,在挂载前可用onBeforeMount
、挂载后onMounted
;更新后onUpdated
;卸载前onBeforeUnmount
、卸载后onUnmounted
。
实战示例:Todo List 组件迁移
接下来,通过一个典型的 Todo List 示例,演示从 React 到 Vue 的完整迁移步骤。在此之前,先准备一个功能简单、结构清晰的 React 版组件。
4.1 React 版 Todo List(初始化)
// 文件:src/components/TodoList.jsx
import React, { useState, useEffect } from 'react';
function TodoItem({ item, onDelete }) {
return (
<li style={{ display: 'flex', alignItems: 'center' }}>
<span style={{ flex: 1 }}>{item.text}</span>
<button onClick={() => onDelete(item.id)}>删除</button>
</li>
);
}
export default function TodoList() {
// 1. 状态:todos 列表和 input 文本
const [todos, setTodos] = useState([]);
const [input, setInput] = useState('');
// 2. 模拟从 localStorage 读取初始列表
useEffect(() => {
const stored = JSON.parse(localStorage.getItem('todos') || '[]');
setTodos(stored);
}, []);
// 3. 更新 localStorage
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);
// 添加函数
const addTodo = () => {
if (!input.trim()) return;
const newItem = { id: Date.now(), text: input.trim() };
setTodos([...todos, newItem]);
setInput('');
};
// 删除函数
const deleteTodo = (id) => {
setTodos(todos.filter((t) => t.id !== id));
};
return (
<div style={{ width: '400px', margin: 'auto' }}>
<h2>Todo List (React)</h2>
<div>
<input
type="text"
value={input}
placeholder="输入待办事项"
onChange={(e) => setInput(e.target.value)}
/>
<button onClick={addTodo}>添加</button>
</div>
<ul>
{todos.map((item) => (
<TodoItem key={item.id} item={item} onDelete={deleteTodo} />
))}
</ul>
</div>
);
}
4.1.1 功能说明
TodoList
组件todos
:待办事项数组,每一项{ id, text }
。input
:输入框文字。useEffect
(无依赖)用于加载本地存储数据。useEffect
(依赖[todos]
)用于 Todos 数组更新时,同步到本地存储。addTodo
、新建一条并更新数组。deleteTodo
、通过 id 过滤删除。
TodoItem
子组件- 接收
item
与onDelete
函数,渲染单个待办并绑定删除事件。
- 接收
4.2 Vue 版 Todo List(迁移成果)
<!-- 文件:src/components/TodoList.vue -->
<template>
<div class="container">
<h2>Todo List (Vue)</h2>
<div class="input-area">
<input
type="text"
v-model="input"
placeholder="输入待办事项"
@keyup.enter="addTodo"
/>
<button @click="addTodo">添加</button>
</div>
<ul>
<TodoItem
v-for="item in todos"
:key="item.id"
:item="item"
@delete-item="deleteTodo"
/>
</ul>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue';
import TodoItem from './TodoItem.vue';
const todos = ref([]);
const input = ref('');
// 1. 初始读取 localStorage
onMounted(() => {
const stored = JSON.parse(localStorage.getItem('todos') || '[]');
todos.value = stored;
});
// 2. 监控 todos 变化,同步到 localStorage
watch(
todos,
(newTodos) => {
localStorage.setItem('todos', JSON.stringify(newTodos));
},
{ deep: true }
);
// 添加函数
function addTodo() {
if (!input.value.trim()) return;
const newItem = { id: Date.now(), text: input.value.trim() };
todos.value.push(newItem);
input.value = '';
}
// 删除函数(通过事件触发)
function deleteTodo(id) {
todos.value = todos.value.filter((t) => t.id !== id);
}
</script>
<style scoped>
.container {
width: 400px;
margin: auto;
}
.input-area {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
input {
flex: 1;
padding: 4px 8px;
}
button {
padding: 4px 12px;
}
ul {
padding-left: 0;
}
</style>
<!-- 文件:src/components/TodoItem.vue -->
<template>
<li class="item">
<span>{{ item.text }}</span>
<button @click="$emit('delete-item', item.id)">删除</button>
</li>
</template>
<script setup>
import { defineProps } from 'vue';
const props = defineProps({
item: {
type: Object,
required: true
}
});
</script>
<style scoped>
.item {
display: flex;
align-items: center;
padding: 4px 0;
}
.item span {
flex: 1;
}
button {
padding: 2px 8px;
}
</style>
4.2.1 功能对比
- Vue 用到的 API:
ref
、onMounted
、watch
、v-model
、v-for
、@click
、$emit
。 - Vue 数据都挂在
ref.value
,模板里直接写todos
、input
(Vue 自动解包); - 事件改为
$emit('delete-item', item.id)
,父组件通过@delete-item="deleteTodo"
接收。 v-model="input"
在回车时也绑定了addTodo
,提升用户体验。
4.3 迁移步骤详解
下面细化从 React 版到 Vue 版的每一步转换思路。
4.3.1 将 JSX 转成 Vue 模板
React JSX(片段):
<div style={{ width: '400px', margin: 'auto' }}> <h2>Todo List (React)</h2> <div> <input type="text" value={input} placeholder="输入待办事项" onChange={(e) => setInput(e.target.value)} /> <button onClick={addTodo}>添加</button> </div> <ul> {todos.map((item) => ( <TodoItem key={item.id} item={item} onDelete={deleteTodo} /> ))} </ul> </div>
Vue 模板(对应代码):
<div class="container"> <h2>Todo List (Vue)</h2> <div class="input-area"> <input type="text" v-model="input" placeholder="输入待办事项" @keyup.enter="addTodo" /> <button @click="addTodo">添加</button> </div> <ul> <TodoItem v-for="item in todos" :key="item.id" :item="item" @delete-item="deleteTodo" /> </ul> </div>
最外层容器:
- React:
<div style={{ width: '400px', margin: 'auto' }}>
- Vue:利用 CSS(
<style scoped>
)把.container
设置为同样宽度与居中。
- React:
输入框绑定:
- React:
value={input}
+onChange={(e) => setInput(e.target.value)}
。 - Vue:
v-model="input"
一行搞定双向绑定,并且扩展了对回车的监听(@keyup.enter="addTodo"
)。
- React:
事件绑定:
- React:
onClick={addTodo}
、onChange={...}
。 - Vue:统一用
@click="addTodo"
、@keyup.enter="addTodo"
。
- React:
循环渲染:
- React:
{todos.map(item => <TodoItem key={item.id} ... />)}
。 - Vue:
<TodoItem v-for="item in todos" :key="item.id" ... />
,并把传递 prop 改为:item="item"
,事件回调从onDelete={deleteTodo}
变成$emit('delete-item', ...)
+ 父组件@delete-item="deleteTodo"
。
- React:
4.3.2 状态管理:useState → ref/reactive
React 用法:
const [todos, setTodos] = useState([]); const [input, setInput] = useState('');
Vue 对应:
import { ref } from 'vue'; const todos = ref([]); const input = ref('');
要点:
- React
todos
是普通数组,更新时需调用setTodos(newArray)
。 - Vue
todos.value
是数组;如果用.push()
、.splice()
等操作,Vue 会拦截并自动触发视图更新。若要整个重置数组,可以直接todos.value = [...]
。
4.3.3 事件绑定:onClick → @click
- React:
<button onClick={deleteTodo}>删除</button>
- Vue:
<button @click="deleteTodo(item.id)">删除</button>
要点:
- React 的事件属性都是驼峰式,比如
onClick
、onChange
;Vue 则是@click
、@change
,或者完整写成v-on:click
、v-on:change
。 - 回调写法也要从 JSX 插值(
{}
)切换到模板表达式(""
),并注意:在 Vue 模板里访问的是组件实例作用域下的函数或属性。
4.3.4 Props 与事件传递
React 里,父组件写:
<TodoItem key={item.id} item={item} onDelete={deleteTodo} />
子组件:
function TodoItem({ item, onDelete }) { return ( <li> … <button onClick={() => onDelete(item.id)}>删除</button> </li> ); }
Vue 里,父组件写:
<TodoItem v-for="item in todos" :key="item.id" :item="item" @delete-item="deleteTodo" />
子组件:
<template> <li class="item"> <span>{{ item.text }}</span> <button @click="$emit('delete-item', item.id)">删除</button> </li> </template> <script setup> import { defineProps } from 'vue'; const props = defineProps({ item: { type: Object, required: true } }); </script>
要点:
Prop 传值:
- React:
item={item}
;子组件通过函数参数拿取。 - Vue:
:item="item"
;子组件通过defineProps
解构 props 对象拿取。
- React:
事件回调:
- React:父组件把函数
deleteTodo
当做 proponDelete
传给子,子组件里直接调用onDelete(item.id)
。 - Vue:子组件通过
$emit('delete-item', item.id)
派发事件,父组件通过@delete-item="deleteTodo"
监听并执行。
- React:父组件把函数
命名规范:
- React 可以自由命名 prop,常用驼峰式:
onDelete
。 - Vue 提倡事件名用中划线分隔(kebab-case),模板里必须一致:
@delete-item
。组件内部若用emits
验证,可书写['delete-item']
。
- React 可以自由命名 prop,常用驼峰式:
4.3.5 生命周期钩子替换
React:
useEffect(() => { // 组件挂载后的读取 const stored = JSON.parse(localStorage.getItem('todos') || '[]'); setTodos(stored); }, []); useEffect(() => { // todos 变化后写入 localStorage localStorage.setItem('todos', JSON.stringify(todos)); }, [todos]);
Vue:
import { onMounted, watch } from 'vue'; onMounted(() => { const stored = JSON.parse(localStorage.getItem('todos') || '[]'); todos.value = stored; }); watch( todos, (newTodos) => { localStorage.setItem('todos', JSON.stringify(newTodos)); }, { deep: true } );
要点:
- 组件挂载后:React
useEffect(..., [])
→ VueonMounted(...)
。 - 监测依赖变化:React
useEffect(..., [todos])
→ Vuewatch(todos, callback, { deep: true })
。 - Vue 的
watch
默认不会深度监听嵌套对象,需{ deep: true }
,但针对数组这种一维结构可省去deep
。不过为了保险,示例加了deep: true
。 - 若需要在组件销毁时做清理,Vue 可用
onUnmounted(...)
,而 React 则在useEffect
返回的函数中。
高级迁移策略
当项目较大,包含路由、状态管理、复杂的 Hooks 逻辑等,需要更系统的迁移思路。下面列出几种常见场景及建议做法。
5.1 Hooks 模式到 Composition API
React Hooks:自定义 Hook 把复用逻辑封装成函数,返回 state、方法等。
// useFetchData.js import { useState, useEffect } from 'react'; export function useFetchData(url) { const [data, setData] = useState(null); useEffect(() => { fetch(url) .then((r) => r.json()) .then((json) => setData(json)); }, [url]); return data; }
Vue Composition API:同样把复用逻辑封装成函数,但需要返回
ref
、computed
、方法等。// useFetchData.js import { ref, watchEffect } from 'vue'; export function useFetchData(url) { const data = ref(null); watchEffect(async () => { if (url.value) { const res = await fetch(url.value); data.value = await res.json(); } }); return { data }; }
- 注:如果
url
是一个纯字符串,可直接传入;若在组件中需要动态响应,则可把url
定义为ref
再传。
- 注:如果
迁移要点:
- React 中自定义 Hook 里用
useState/useEffect
,Vue 里用ref/reactive
+onMounted
或watch
/watchEffect
。- 返回的对象都要包含“数据”与“方法”,供组件直接解构使用。
- React Hook 每次都要写依赖数组,Vue 的
watchEffect
则会自动跟踪依赖。
5.2 Redux / Context 到 Vuex / Pinia
- React Redux:在组件中用
useSelector
、useDispatch
;自定义 Action、Reducer。 Vuex(3.x/4.x)或 Pinia(推荐):
- Vuex:类似 Redux,需要手动定义
state
、mutations
、actions
、getters
,并用mapState
、mapActions
在组件里拿到。 - Pinia:更贴近 Composition API,使用
defineStore
定义 store,组件可直接用useStore = defineStore(...)
拿到,访问属性就像访问普通对象。
- Vuex:类似 Redux,需要手动定义
// Pinia 示例:src/stores/todoStore.js
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useTodoStore = defineStore('todos', () => {
const todos = ref([]);
function addTodo(text) {
todos.value.push({ id: Date.now(), text });
}
function removeTodo(id) {
todos.value = todos.value.filter((t) => t.id !== id);
}
return { todos, addTodo, removeTodo };
});
迁移要点:
- 如果之前在 React 里用 Redux,只需把各个 Action/Reducer 概念迁移成 Pinia 的
actions
和state
。- 组件里不再使用
useDispatch
、useSelector
,而是直接const todoStore = useTodoStore()
,并且用todoStore.todos
、todoStore.addTodo()
。- Pinia 的热重载与 DevTools 支持比 Vuex 更友好,建议直接采用 Pinia。
5.3 第三方库适配(路由、请求库等)
路由
React Router → Vue Router
- React Router:
<BrowserRouter>
、<Route path="/" element={<Home />} />
。 - Vue Router: 在
router/index.js
里定义createRouter({ history: createWebHistory(), routes: [...] })
,组件里用<router-link>
、<router-view>
。
- React Router:
请求库
- Axios、Fetch 在两端是一致的,不需要迁移。
- 若用 React Query,可考虑在 Vue 里用 Vue Query 或直接用 Composition API 手动封装。
UI 组件库
- Ant Design React → Ant Design Vue(API 大同小异)。
- Material-UI → Vuetify 或 Element Plus 等,根据团队偏好选择替代。迁移时注意 API 差异,比如组件属性名、主题配置项。
迁移要点:
- 路由:需要重写配置文件,组件内切换页面的逻辑也要由
<Link to="/path">
换成<router-link to="/path">
,并在 JS 里用useNavigate()
→useRouter().push()
。- 请求:一般不用改,兼容性好。
- UI 组件库:需要整体替换,组件名、属性、插槽机制都要检查并重写。
常见痛点与解决方案
JSX 表达式复杂逻辑 → Vue 模板写不下
- 现象:在 React 里,复杂逻辑直接写在 JSX 里,比如三元表达式嵌套。Vue 模板写会显得啰嗦。
- 解决:把逻辑抽离到
<script setup>
里的计算属性computed
或函数里,在模板里只调用。
<script setup> import { computed } from 'vue'; const items = ref([/* ... */]); const filtered = computed(() => { return items.value.filter((i) => i.active).map((i) => /* ... */); }); </script> <template> <div v-for="item in filtered" :key="item.id"> {{ item.name }} </div> </template>
React Context → Vue Provide / Inject
- 现象:React 用 Context 共享状态,Vue 却对新手较陌生。
- 解决:Vue 里在父组件用
provide('key', value)
,在子组件里inject('key')
。若用 Pinia,更建议直接把共享状态放到 Store 里。
Hooks 依赖数组遗漏 → 逻辑难以调试
- 现象:Vue 的
watch
依赖也有类似问题,需加deep
。 - 解决:在关键路径写单独的
watchEffect
,并在必要时手动停止监听(const stop = watch(...); stop()
)。
- 现象:Vue 的
组件样式隔离
- 现象:React 用 CSS Modules、Styled Components,Vue 用
<style scoped>
或 CSS Modules。 - 解决:在 Vue 里,保留
<style scoped>
,也可以用 CSS Modules,写法为<style module>
,然后在模板里使用:class="$style.className"
。
- 现象:React 用 CSS Modules、Styled Components,Vue 用
总结
本文详细剖析了从 React 迁移到 Vue 的各个关键点:
- 核心理念对比:响应式 vs 函数式更新、JSX vs 模板、生命周期钩子映射。
- 概念图解:通过 ASCII 示意图直观理解数据流与生命周期差异。
- 实战示例:一步步把 React 版 Todo List 拆解、迁移到 Vue 版,涵盖模板、状态、事件、Props、生命周期等核心内容。
- 高级策略:包括 Hooks → Composition API、Redux → Pinia、路由与 UI 库替换的实践建议。
- 常见痛点:针对繁琐逻辑、Context、依赖监听、样式隔离等迁移难题给出解决方案。
完成迁移的关键在于:
- 找准映射关系:把 React 的 Hook、JSX、Context、Redux 等概念,对应到 Vue 的 Composition API、模板语法、Provide/Inject、Pinia。
- 分阶段逐步替换:先完成最核心的组件渲染、状态更新,然后再处理路由、状态管理等外部依赖。
- 善用 Vue 高级特性:合理运用
ref
/reactive
、computed
、watchEffect
,以及<script setup>
带来的简洁写法,让迁移后的代码保持高可读性。
希望本文能够帮助你快速搭建 “React → Vue” 的迁移桥梁,让你在新旧框架之间游刃有余。