Vue 2 vs React 18 深度对比指南
本文档面向熟练使用 Vue 2 的开发者,帮助快速理解 React 18 的核心概念与差异。
目录
- 核心设计哲学
- 组件定义
- 响应式原理
- [模板 vs JSX](#模板 vs JSX "#4-%E6%A8%A1%E6%9D%BF-vs-jsx")
- 生命周期对比
- [计算属性 vs useMemo](#计算属性 vs useMemo "#6-%E8%AE%A1%E7%AE%97%E5%B1%9E%E6%80%A7-vs-usememo")
- [侦听器 vs useEffect](#侦听器 vs useEffect "#7-%E4%BE%A6%E5%90%AC%E5%99%A8-vs-useeffect")
- 父子通信
- 跨层级通信
- [插槽 vs children / render props](#插槽 vs children / render props "#10-%E6%8F%92%E6%A7%BD-vs-children--render-props")
- [虚拟 DOM 与 Diff 算法](#虚拟 DOM 与 Diff 算法 "#11-%E8%99%9A%E6%8B%9F-dom-%E4%B8%8E-diff-%E7%AE%97%E6%B3%95")
- [React 18 新特性](#React 18 新特性 "#12-react-18-%E6%96%B0%E7%89%B9%E6%80%A7")
- 性能优化对比
- 总结对照表
- 迁移建议
1. 核心设计哲学
| 维度 | Vue 2 | React 18 |
|---|---|---|
| 定位 | 渐进式框架(框架帮你做更多) | UI 库(你自己组合生态) |
| 模板 | 模板语法 + 指令 | JSX(JS 的语法扩展) |
| 响应式 | 自动依赖追踪(getter/setter) | 手动声明更新(setState) |
| 心智模型 | "数据变了,视图自动变" | "调用更新函数,触发重新渲染" |
2. 组件定义
Vue 2:选项式 API
vue
<template>
<div>{{ count }}</div>
</template>
<script>
export default {
data() {
return { count: 0 }
},
methods: {
increment() {
this.count++
}
}
}
</script>
React 18:函数组件 + Hooks
tsx
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
const increment = () => setCount(c => c + 1)
return <div>{count}</div>
}
原理差异
- Vue 2 :组件是一个"配置对象",Vue 内部实例化并管理生命周期。
this指向组件实例,data会被 Vue 用Object.defineProperty转成响应式。 - React 18:组件就是一个"纯函数",每次渲染都会重新执行。状态通过 Hooks 保存在 React 内部的 Fiber 节点上,而不是组件实例上。
3. 响应式原理
Vue 2:基于 Object.defineProperty 的依赖追踪
text
数据变化流程:
data → defineProperty(getter/setter)
↓
getter 收集依赖(Watcher)
↓
setter 触发依赖更新
↓
Watcher 通知组件重新渲染
核心代码逻辑(简化)
js
// Vue 2 响应式核心
function defineReactive(obj, key, val) {
const dep = new Dep() // 依赖收集器
Object.defineProperty(obj, key, {
get() {
if (Dep.target) {
dep.depend() // 收集当前 Watcher
}
return val
},
set(newVal) {
if (newVal === val) return
val = newVal
dep.notify() // 通知所有 Watcher 更新
}
})
}
局限性:
- 无法检测属性的添加/删除(需要
Vue.set) - 无法检测数组索引赋值(需要用
splice等变异方法)
React 18:不可变数据 + 调度更新
text
状态变化流程:
setState(newValue)
↓
React 调度器标记组件需要更新
↓
批量处理更新(Batching)
↓
重新执行函数组件
↓
Diff 虚拟 DOM → 更新真实 DOM
核心机制:
js
// React 状态更新(简化)
function useState(initialValue) {
// 状态存储在 Fiber 节点的 memoizedState 链表上
const hook = mountWorkInProgressHook()
hook.memoizedState = initialValue
const dispatch = (action) => {
// 创建更新对象,加入更新队列
const update = { action, next: null }
enqueueUpdate(hook.queue, update)
// 调度更新
scheduleUpdateOnFiber(fiber)
}
return [hook.memoizedState, dispatch]
}
React 18 新特性:自动批处理(Automatic Batching)
js
// React 17:只有事件处理函数内会批处理
// React 18:所有更新都会自动批处理
// 以下三次 setState 只会触发一次重渲染
setTimeout(() => {
setCount(c => c + 1)
setFlag(f => !f)
setName('new')
}, 1000)
4. 模板 vs JSX
Vue 2:模板 + 指令
vue
<template>
<div>
<!-- 条件渲染 -->
<span v-if="show">显示</span>
<span v-else>隐藏</span>
<!-- 列表渲染 -->
<ul>
<li v-for="item in list" :key="item.id">
{{ item.name }}
</li>
</ul>
<!-- 双向绑定 -->
<input v-model="text" />
<!-- 事件 -->
<button @click="handleClick">点击</button>
</div>
</template>
原理:
- 模板在编译阶段被转换为渲染函数(
render function) - 指令(
v-if、v-for)是编译时的语法糖 - 编译器可以做静态分析优化(标记静态节点)
React 18:JSX
tsx
function MyComponent({ show, list, text, setText }) {
return (
<div>
{/* 条件渲染 */}
{show ? <span>显示</span> : <span>隐藏</span>}
{/* 列表渲染 */}
<ul>
{list.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
{/* 受控组件(双向绑定) */}
<input value={text} onChange={e => setText(e.target.value)} />
{/* 事件 */}
<button onClick={handleClick}>点击</button>
</div>
)
}
原理:
- JSX 是
React.createElement()的语法糖 - 编译后:
<div>hello</div>→React.createElement('div', null, 'hello') - 没有指令,一切都是 JS 表达式
语法对照表
| 特性 | Vue 2 模板 | React JSX |
|---|---|---|
| 条件 | v-if / v-else |
{condition && ...} 或三元 |
| 循环 | v-for |
array.map() |
| 双向绑定 | v-model |
受控组件(value + onChange) |
| 事件 | @click |
onClick |
| 样式 | :class / :style |
className / style={{}} |
5. 生命周期对比
Vue 2 生命周期
text
beforeCreate → created → beforeMount → mounted
↓
beforeUpdate → updated
↓
beforeDestroy → destroyed
js
export default {
created() {
// 实例创建完成,data/methods 可用,DOM 未挂载
// 常用于:初始化数据、发起请求
},
mounted() {
// DOM 已挂载
// 常用于:操作 DOM、初始化第三方库
},
updated() {
// 数据变化导致 DOM 更新后
},
beforeDestroy() {
// 销毁前,清理定时器、事件监听等
}
}
React 18:useEffect 统一处理
tsx
import { useEffect, useLayoutEffect } from 'react'
function MyComponent() {
// 相当于 mounted + updated
useEffect(() => {
console.log('组件挂载或更新后')
// 相当于 beforeDestroy
return () => {
console.log('清理:组件卸载前 或 下次 effect 执行前')
}
}) // 无依赖数组:每次渲染后都执行
// 相当于 mounted(只执行一次)
useEffect(() => {
console.log('只在挂载时执行')
return () => console.log('只在卸载时执行')
}, []) // 空依赖数组
// 相当于 watch
useEffect(() => {
console.log('count 变化了')
}, [count]) // 依赖 count
return <div>...</div>
}
原理:
useEffect的回调在 DOM 更新后异步执行(不阻塞渲染)useLayoutEffect在 DOM 更新后同步执行(阻塞渲染,用于测量 DOM)- 依赖数组决定何时重新执行 effect
生命周期对照表
| Vue 2 | React 18 |
|---|---|
created |
函数体顶部(但要注意 SSR) |
mounted |
useEffect(() => {}, []) |
updated |
useEffect(() => {}) 或 useEffect(() => {}, [deps]) |
beforeDestroy |
useEffect 返回的清理函数 |
watch |
useEffect(() => {}, [watchedValue]) |
6. 计算属性 vs useMemo
Vue 2:computed
js
export default {
data() {
return { firstName: 'John', lastName: 'Doe' }
},
computed: {
fullName() {
return `${this.firstName} ${this.lastName}`
}
}
}
原理:
- computed 是一个惰性求值的 Watcher
- 只有依赖变化时才重新计算
- 有缓存:多次访问不会重复计算
React 18:useMemo
tsx
function MyComponent({ firstName, lastName }) {
const fullName = useMemo(() => {
return `${firstName} ${lastName}`
}, [firstName, lastName])
return <div>{fullName}</div>
}
原理:
useMemo在依赖数组不变时返回缓存值- 依赖数组变化时重新执行计算函数
- 必须手动声明依赖(Vue 是自动追踪)
关键区别
| 特性 | Vue 2 computed | React useMemo |
|---|---|---|
| 依赖追踪 | 自动 | 手动声明 |
| 缓存 | 有 | 有 |
| 用途 | 派生状态 | 派生状态 + 避免重复计算 |
7. 侦听器 vs useEffect
Vue 2:watch
js
export default {
data() {
return { query: '' }
},
watch: {
query: {
handler(newVal, oldVal) {
this.search(newVal)
},
immediate: true, // 立即执行
deep: true // 深度监听
}
}
}
React 18:useEffect
tsx
function SearchComponent() {
const [query, setQuery] = useState('')
useEffect(() => {
// 没有 oldVal,需要自己用 useRef 保存
search(query)
}, [query])
return <input value={query} onChange={e => setQuery(e.target.value)} />
}
获取旧值的方式:
tsx
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>()
useEffect(() => {
ref.current = value
})
return ref.current
}
// 使用
const prevQuery = usePrevious(query)
8. 父子通信
Vue 2
vue
<!-- 父组件 -->
<template>
<Child :msg="message" @update="handleUpdate" />
</template>
<!-- 子组件 -->
<template>
<div @click="$emit('update', newValue)">{{ msg }}</div>
</template>
<script>
export default {
props: ['msg']
}
</script>
React 18
tsx
// 父组件
function Parent() {
const [message, setMessage] = useState('')
return (
<Child
msg={message}
onUpdate={(newValue) => setMessage(newValue)}
/>
)
}
// 子组件
interface ChildProps {
msg: string
onUpdate: (value: string) => void
}
function Child({ msg, onUpdate }: ChildProps) {
return <div onClick={() => onUpdate('new')}>{msg}</div>
}
通信方式对照
| 特性 | Vue 2 | React 18 |
|---|---|---|
| 父→子 | props |
props |
| 子→父 | $emit |
回调函数 props |
| 双向绑定 | v-model / .sync |
受控组件模式 |
9. 跨层级通信
Vue 2:provide / inject
js
// 祖先组件
export default {
provide() {
return {
theme: this.theme
}
}
}
// 后代组件
export default {
inject: ['theme']
}
注意: Vue 2 的 provide/inject 不是响应式的(除非 provide 一个响应式对象)。
React 18:Context
tsx
// 创建 Context
const ThemeContext = createContext<string>('light')
// 祖先组件
function App() {
const [theme, setTheme] = useState('dark')
return (
<ThemeContext.Provider value={theme}>
<Child />
</ThemeContext.Provider>
)
}
// 后代组件
function DeepChild() {
const theme = useContext(ThemeContext)
return <div>当前主题:{theme}</div>
}
原理
- Provider 的 value 变化时,所有消费该 Context 的组件都会重新渲染
- 这是 React 的一个性能陷阱:Context 变化会导致所有消费者重渲染,即使它们只用了 Context 的一部分
优化方式:
- 拆分 Context(读写分离)
- 使用
useMemo包裹 value - 或使用状态管理库(Redux/Zustand)
10. 插槽 vs children / render props
Vue 2:插槽
vue
<!-- 父组件 -->
<Card>
<template #header>标题</template>
<template #default>内容</template>
<template #footer="{ data }">{{ data }}</template>
</Card>
<!-- Card 组件 -->
<template>
<div>
<header><slot name="header" /></header>
<main><slot /></main>
<footer><slot name="footer" :data="footerData" /></footer>
</div>
</template>
React 18:children + render props
tsx
// 父组件
<Card
header={<span>标题</span>}
footer={(data) => <span>{data}</span>}
>
内容
</Card>
// Card 组件
interface CardProps {
header?: ReactNode
footer?: (data: string) => ReactNode
children: ReactNode
}
function Card({ header, footer, children }: CardProps) {
const footerData = 'some data'
return (
<div>
<header>{header}</header>
<main>{children}</main>
<footer>{footer?.(footerData)}</footer>
</div>
)
}
插槽对照
| Vue 2 | React 18 |
|---|---|
默认插槽 <slot /> |
children |
具名插槽 <slot name="x" /> |
具名 props(如 header) |
| 作用域插槽 | render props(函数作为 props) |
11. 虚拟 DOM 与 Diff 算法
Vue 2 Diff
- 双端比较算法:同时从新旧节点列表的两端向中间比较
- 优化:静态节点标记,跳过不变的节点
text
旧: [A, B, C, D]
新: [D, A, B, C]
双端比较:
1. 旧头(A) vs 新头(D) ❌
2. 旧尾(D) vs 新尾(C) ❌
3. 旧头(A) vs 新尾(C) ❌
4. 旧尾(D) vs 新头(D) ✅ → 移动 D 到最前
React 18 Diff
- 单向遍历 + key 映射
- 只从左到右遍历,通过 key 建立映射
text
旧: [A, B, C, D]
新: [D, A, B, C]
1. 遍历新列表,D 在旧列表中找到,但位置不对
2. 标记需要移动的节点
3. 最小化 DOM 操作
关键:key 的作用
tsx
// ❌ 错误:用 index 作为 key
{list.map((item, index) => <Item key={index} />)}
// ✅ 正确:用唯一标识作为 key
{list.map(item => <Item key={item.id} />)}
12. React 18 新特性
12.1 并发渲染(Concurrent Rendering)
React 18 最大的变化:渲染可以被中断。
tsx
import { useTransition, useDeferredValue } from 'react'
function SearchResults() {
const [query, setQuery] = useState('')
const [isPending, startTransition] = useTransition()
const handleChange = (e) => {
// 紧急更新:输入框立即响应
setQuery(e.target.value)
// 非紧急更新:搜索结果可以延迟
startTransition(() => {
setSearchResults(search(e.target.value))
})
}
return (
<>
<input value={query} onChange={handleChange} />
{isPending ? <Spinner /> : <Results />}
</>
)
}
原理:
- React 18 引入了优先级调度
startTransition标记的更新是低优先级的,可以被高优先级更新打断- 用户输入等交互是高优先级,数据渲染是低优先级
12.2 Suspense 数据获取
tsx
// 配合 React Query / SWR / Relay 等
function ProfilePage() {
return (
<Suspense fallback={<Spinner />}>
<ProfileDetails />
<Suspense fallback={<PostsSpinner />}>
<ProfilePosts />
</Suspense>
</Suspense>
)
}
12.3 自动批处理
tsx
// React 18:所有更新自动批处理
setTimeout(() => {
setCount(c => c + 1) // 不会立即渲染
setFlag(f => !f) // 不会立即渲染
// 只触发一次渲染
}, 1000)
13. 性能优化对比
Vue 2 性能优化
js
// 1. v-once:只渲染一次
<span v-once>{{ staticContent }}</span>
// 2. v-memo(Vue 3.2+,Vue 2 没有)
// 3. computed 自带缓存
// 4. keep-alive 缓存组件
<keep-alive>
<component :is="currentComponent" />
</keep-alive>
React 18 性能优化
tsx
// 1. React.memo:组件级别缓存
const MemoizedComponent = React.memo(function MyComponent(props) {
return <div>{props.value}</div>
})
// 2. useMemo:值缓存
const expensiveValue = useMemo(() => compute(a, b), [a, b])
// 3. useCallback:函数缓存
const handleClick = useCallback(() => {
doSomething(a, b)
}, [a, b])
// 4. 懒加载
const LazyComponent = React.lazy(() => import('./HeavyComponent'))
<Suspense fallback={<Loading />}>
<LazyComponent />
</Suspense>
优化方式对照
| 优化点 | Vue 2 | React 18 |
|---|---|---|
| 组件缓存 | 自动(响应式追踪) | 手动(React.memo) |
| 计算缓存 | computed(自动依赖) |
useMemo(手动依赖) |
| 函数缓存 | 不需要(方法绑定在实例上) | useCallback(避免子组件重渲染) |
| 组件保活 | <keep-alive> |
无内置,需第三方库 |
14. 总结对照表
| 特性 | Vue 2 | React 18 |
|---|---|---|
| 组件定义 | 选项式对象 | 函数 + Hooks |
| 响应式 | 自动(defineProperty) | 手动(setState) |
| 模板 | 模板 + 指令 | JSX |
| 状态 | data() |
useState |
| 计算属性 | computed |
useMemo |
| 侦听 | watch |
useEffect |
| 生命周期 | 多个钩子函数 | useEffect 统一 |
| 父子通信 | props + $emit | props + 回调 |
| 跨层级 | provide/inject | Context |
| 插槽 | slot | children / render props |
| 性能优化 | 框架自动优化多 | 开发者手动优化多 |
| 并发 | 无 | Concurrent Mode |
| 学习曲线 | 平缓 | 陡峭(Hooks 心智模型) |
15. 迁移建议
从 Vue 2 转 React 18,重点转变思维:
15.1 从"响应式"到"不可变"
js
// Vue:直接修改
this.list.push(item)
// React:创建新数组
setList([...list, item])
15.2 从"自动依赖"到"手动声明"
js
// Vue:computed 自动追踪依赖
computed: {
fullName() {
return this.firstName + this.lastName
}
}
// React:useMemo 必须手动写依赖数组
const fullName = useMemo(() => {
return firstName + lastName
}, [firstName, lastName])
15.3 从"实例方法"到"闭包函数"
js
// Vue:this.handleClick 始终是同一个函数
methods: {
handleClick() { ... }
}
// React:每次渲染 handleClick 都是新函数(需要 useCallback 优化)
const handleClick = useCallback(() => {
doSomething(a, b)
}, [a, b])
15.4 从"模板指令"到"JS 表达式"
| Vue 2 | React 18 |
|---|---|
v-if="show" |
{show && <Component />} |
v-for="item in list" |
{list.map(item => ...)} |
v-model="value" |
value={value} onChange={...} |
附录:常用 Hooks 速查
| Hook | 用途 | Vue 2 对应 |
|---|---|---|
useState |
状态管理 | data |
useEffect |
副作用处理 | mounted / watch / beforeDestroy |
useMemo |
计算缓存 | computed |
useCallback |
函数缓存 | 无(Vue 不需要) |
useRef |
引用 DOM / 保存可变值 | $refs / 实例属性 |
useContext |
跨层级状态 | inject |
useReducer |
复杂状态逻辑 | Vuex mutations |
useLayoutEffect |
同步副作用 | mounted(同步部分) |
useTransition |
并发更新 | 无 |
useDeferredValue |
延迟更新 | 无 |