核心概念对比
从响应式原理到模板语法,逐一对比 Vue 2 与 React 的核心概念差异,建立心智模型映射。
概念速查表
Vue 2 到 React 的核心概念一一映射
| Vue 2 概念 | React 对应 | 说明 |
|---|---|---|
data() 返回响应式对象 |
useState Hook |
状态声明方式不同,Vue 是对象,React 是独立变量 |
computed |
useMemo |
计算/派生状态,依赖变化时重新计算 |
watch |
useEffect |
副作用监听,但 React 的 useEffect 语义更广 |
methods |
普通函数 / useCallback |
Vue 的 methods 挂在实例上,React 直接定义在函数体内 |
mounted |
useEffect([], ...) |
组件挂载后执行,依赖数组传空数组 |
updated |
useEffect 无依赖 |
每次渲染后执行,需注意避免死循环 |
beforeDestroy |
useEffect 返回函数 |
清理函数在组件卸载前执行 |
v-model |
受控组件 + onChange |
Vue 双向绑定,React 需手动同步状态 |
v-if / v-show |
条件渲染(&&& / 三元) |
v-if 对应条件渲染,v-show 对应 CSS display |
v-for |
array.map() |
列表渲染,React 需要显式指定 key |
ref |
useRef |
DOM 引用,React 的 useRef 也可存储任意可变值 |
provide/inject |
Context |
跨层级通信,避免 props 逐层传递 |
这张表是你的核心参考。迁移时不必一次理解全部差异,建议按模块逐步对照,先从 data → useState 开始建立直觉。
响应式原理对比
理解两种框架的状态追踪机制差异
Vue 2 使用 Object.defineProperty 递归劫持 data 对象的每个属性,在 getter 中收集依赖,在 setter 中通知更新。
无法检测新增属性(需 Vue.set)和数组索引修改。
React 不做数据劫持。调用 setState 时创建新引用,触发组件重新渲染,由开发者决定何时以及如何更新。
对象/数组必须创建新引用,否则 React 认为没有变化。
// Vue 2 - 响应式数据自动追踪
export default {
data() {
return {
count: 0,
user: { name: 'Alice', age: 25 }
}
},
computed: {
// 自动追踪 this.count 的变化
doubleCount() {
return this.count * 2
}
},
methods: {
increment() {
this.count++ // 自动触发视图更新
},
addProp() {
// 无法检测!需要用 Vue.set
this.$set(this.user, 'email', 'a@b.com')
}
}
}
import { useState, useMemo } from 'react'
interface User {
name: string
age: number
email?: string
}
function Counter() {
const [count, setCount] = useState<number>(0)
const [user, setUser] = useState<User>({
name: 'Alice', age: 25
})
// 手动追踪 count 的派生值
const doubleCount = useMemo(
() => count * 2,
[count]
)
const increment = () => {
setCount(prev => prev + 1)
}
const addProp = () => {
// 创建新对象即可,无需特殊 API
setUser(prev => ({ ...prev, email: 'a@b.com' }))
}
}
生命周期映射
从 Options API 钩子到 useEffect 的思维转换
Vue 2 的这两个钩子对应 React 函数组件的"函数体本身"——组件函数执行即代表初始化。
组件挂载到 DOM 后执行。依赖数组传空数组,确保只在首次渲染后运行一次。
省略依赖数组时,每次渲染后都会执行。注意不要在其中修改状态导致死循环。
指定依赖数组可以精确监听特定值的变化,类似 Vue 的 watch 选项。
清理副作用。useEffect 返回的函数在组件卸载前和下次 effect 执行前调用。
错误边界目前只能用 class 组件实现,用于捕获子组件的渲染错误。
export default {
data() {
return { items: [], timer: null }
},
// 组件挂载后
mounted() {
this.fetchItems()
this.timer = setInterval(() => {
this.fetchItems()
}, 5000)
},
// 监听特定数据变化
watch: {
items(newVal, oldVal) {
console.log('items changed', newVal.length)
}
},
// 组件销毁前清理
beforeDestroy() {
if (this.timer) {
clearInterval(this.timer)
}
}
}
import { useState, useEffect } from 'react'
interface Item {
id: string
name: string
}
function ItemList() {
const [items, setItems] = useState<Item[]>([])
// mounted + beforeDestroy 合并到一个 effect
useEffect(() => {
fetchItems()
const timer = setInterval(() => {
fetchItems()
}, 5000)
// cleanup: 等价于 beforeDestroy
return () => {
clearInterval(timer)
}
}, []) // 空依赖 = 只在 mount/unmount 时执行
// watch items 的变化
useEffect(() => {
console.log('items changed', items.length)
}, [items]) // 依赖数组 = watch 目标
}
Vue 的 watch 可以拿到旧值和新值,而 useEffect 只能拿到最新值。
如果需要旧值,需要用 useRef 手动保存。
模板语法对比
从 Vue 模板指令到 JSX 表达式的转换
<!-- v-if 完全移除 DOM -->
<div v-if="isLoggedIn">
<p>欢迎回来</p>
</div>
<div v-else>
<p>请登录</p>
</div>
<!-- v-show 仅切换 display -->
<div v-show="isVisible">
可见内容
</div>
// && 短路 —— 适合"有或无"
{isLoggedIn && <p>欢迎回来</p>}
// 三元 —— 适合"二选一"
{isLoggedIn
? <p>欢迎回来</p>
: <p>请登录</p>
}
// v-show 等价:CSS 控制
<div style={{ display: isVisible ? 'block' : 'none' }}>
可见内容
</div>
<!-- 基础列表 -->
<ul>
<li
v-for="(item, index) in items"
:key="item.id"
>
{{ index + 1 }}. {{ item.name }}
</li>
</ul>
<!-- 遍历对象 -->
<div
v-for="(value, key) in user"
:key="key"
>
{{ key }}: {{ value }}
</div>
// 基础列表
<ul>
{items.map((item, index) => (
<li key={item.id}>
{index + 1}. {item.name}
</li>
))}
</ul>
// 遍历对象
{Object.entries(user).map(
([key, value]) => (
<div key={key}>
{key}: {value}
</div>
)
)}
<template>
<form @submit.prevent="onSubmit">
<input
v-model="form.name"
placeholder="姓名"
/>
<select v-model="form.role">
<option value="admin">管理员</option>
<option value="user">用户</option>
</select>
<input
type="checkbox"
v-model="form.agree"
/>
</form>
</template>
const [form, setForm] = useState({
name: '',
role: 'admin',
agree: false,
})
const handleChange = (
e: React.ChangeEvent<
HTMLInputElement | HTMLSelectElement
>
) => {
const { name, value, type } = e.target
setForm(prev => ({
...prev,
[name]: type === 'checkbox'
? (e.target as HTMLInputElement).checked
: value
}))
}
<form onSubmit={onSubmit}>
<input
name="name"
value={form.name}
onChange={handleChange}
/>
<select
name="role"
value={form.role}
onChange={handleChange}
>
<option value="admin">管理员</option>
<option value="user">用户</option>
</select>
<input
type="checkbox"
name="agree"
checked={form.agree}
onChange={handleChange}
/>
</form>
export default {
data() {
return {
price: 100,
quantity: 3,
discount: 0.8
}
},
computed: {
// 自动缓存,依赖不变则不重算
total() {
return this.price
* this.quantity
* this.discount
},
formattedTotal() {
return `¥${this.total.toFixed(2)}`
}
}
}
function Cart() {
const [price] = useState(100)
const [quantity] = useState(3)
const [discount] = useState(0.8)
// 依赖不变时返回缓存值
const total = useMemo(
() => price * quantity * discount,
[price, quantity, discount]
)
// 链式派生
const formattedTotal = useMemo(
() => `¥${total.toFixed(2)}`,
[total]
)
}
完整组件对比
一个真实场景:带搜索、过滤、计数器的用户列表
<template>
<div class="user-list">
<h2>用户列表 ({{ filteredCount }})</h2>
<!-- 搜索框:v-model 双向绑定 -->
<input
v-model="search"
placeholder="搜索用户..."
/>
<!-- 条件渲染:加载状态 -->
<div v-if="loading">加载中...</div>
<!-- 列表渲染 -->
<ul v-else>
<li
v-for="user in filteredUsers"
:key="user.id"
@click="selectUser(user)"
>
{{ user.name }}
</li>
</ul>
<!-- 选中用户详情 -->
<div v-if="selectedUser">
<p>选中: {{ selectedUser.name }}</p>
</div>
</div>
</template>
<script>
export default {
data() {
return {
users: [],
search: '',
loading: false,
selectedUser: null,
}
},
computed: {
filteredUsers() {
return this.users.filter(u =>
u.name.toLowerCase()
.includes(this.search.toLowerCase())
)
},
filteredCount() {
return this.filteredUsers.length
}
},
mounted() {
this.fetchUsers()
},
methods: {
async fetchUsers() {
this.loading = true
const res = await fetch('/api/users')
this.users = await res.json()
this.loading = false
},
selectUser(user) {
this.selectedUser = user
}
}
}
</script>
import React, {
useState, useEffect, useMemo, useCallback
} from 'react'
interface User {
id: string
name: string
}
const UserList: React.FC = () => {
const [users, setUsers] = useState<User[]>([])
const [search, setSearch] = useState('')
const [loading, setLoading] = useState(false)
const [selectedUser, setSelectedUser] =
useState<User | null>(null)
// computed: filteredUsers
const filteredUsers = useMemo(() => {
return users.filter(u =>
u.name.toLowerCase()
.includes(search.toLowerCase())
)
}, [users, search])
// computed: filteredCount(直接用派生值)
const filteredCount = filteredUsers.length
// mounted: 获取数据
useEffect(() => {
const fetchUsers = async () => {
setLoading(true)
const res = await fetch('/api/users')
const data = await res.json()
setUsers(data)
setLoading(false)
}
fetchUsers()
}, [])
// methods: 用 useCallback 缓存
const selectUser = useCallback(
(user: User) => setSelectedUser(user),
[]
)
return (
<div className="user-list">
<h2>用户列表 ({filteredCount})</h2>
<input
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="搜索用户..."
/>
{loading ? (
<div>加载中...</div>
) : (
<ul>
{filteredUsers.map(user => (
<li
key={user.id}
onClick={() => selectUser(user)}
>
{user.name}
</li>
))}
</ul>
)}
{selectedUser && (
<div>
<p>选中: {selectedUser.name}</p>
</div>
)}
</div>
)
}
export default UserList
注意对比中的模式:Vue 的 computed 自动缓存 vs React 的 useMemo 需手动声明依赖;
Vue 的 methods 不需要缓存 vs React 的函数每次渲染都重新创建(可用 useCallback 优化)。
React Hooks 速查
迁移时最常用的 Hooks 及其 Vue 对应关系
useState
←
data()
useEffect
←
mounted / watch / beforeDestroy
useMemo
←
computed
useCallback
←
methods (缓存版)
useRef
←
ref / this.$refs
useContext
←
provide / inject
useReducer
←
Vuex (局部)
useLayoutEffect
←
updated (同步)
<template>
<input ref="myInput" />
<button @click="focusInput">
聚焦
</button>
</template>
<script>
export default {
mounted() {
// 挂载后自动聚焦
this.$refs.myInput.focus()
},
methods: {
focusInput() {
this.$refs.myInput.focus()
}
}
}
</script>
import { useRef, useEffect } from 'react'
function FocusInput() {
const inputRef = useRef<HTMLInputElement>(null)
// mounted: 自动聚焦
useEffect(() => {
inputRef.current?!.focus()
}, [])
const focusInput = () => {
inputRef.current?!.focus()
}
return (
<>
<input ref={inputRef} />
<button onClick={focusInput}>
聚焦
</button>
</>
)
}
// 祖先组件:提供数据
export default {
provide() {
return {
theme: this.theme,
updateTheme: this.updateTheme
}
},
data() {
return { theme: 'light' }
},
methods: {
updateTheme(val) {
this.theme = val
}
}
}
// 后代组件:注入数据
export default {
inject: ['theme', 'updateTheme'],
template: `
<div :class="theme">
<button @click="updateTheme('dark')">
切换主题
</button>
</div>
`
}
import React, {
createContext, useContext, useState
} from 'react'
// 1. 创建 Context
interface ThemeContextType {
theme: string
updateTheme: (val: string) => void
}
const ThemeContext = createContext<
ThemeContextType
>({ theme: 'light', updateTheme: () => {} })
// 2. 祖先组件:Provider
function App() {
const [theme, setTheme] = useState('light')
return (
<ThemeContext.Provider
value={{ theme, updateTheme: setTheme }}
>
<Child />
</ThemeContext.Provider>
)
}
// 3. 后代组件:useContext
function Child() {
const { theme, updateTheme } =
useContext(ThemeContext)
return (
<div className={theme}>
<button
onClick={() => updateTheme('dark')}
>
切换主题
</button>
</div>
)
}
心智模型转换
迁移时需要牢记的核心思维差异
- 修改数据即触发视图更新
- 模板是声明式的 HTML 扩展
- 单文件组件 (.vue) 三段式结构
- Options API 组织代码
- 框架管理副作用的生命周期
- 状态不可变,必须通过 setter 更新
- JSX 是 JavaScript 的语法扩展
- 一个函数即一个组件
- Hooks 逻辑复用更灵活
- 开发者显式管理副作用依赖
Vue 帮你做了很多(响应式追踪、依赖收集、自动更新),而 React 把控制权交给你。 这意味着 React 的心智负担更集中在"何时重新执行"上, 但换来了更可预测的数据流和更好的可调试性。