Vue 3 与 React 18+ 核心技术深度对比:从源码到实战
阅读时间 :约 15 分钟 | 适合人群:中高级前端开发者、技术决策者
引言:两条不同的技术哲学路线
Vue 和 React 作为当今最流行的两大前端框架,它们的差异远不止"模板 vs JSX"这么简单。深入到底层实现,你会发现它们代表了两种截然不同的技术哲学:
- Vue 3 :自动化、隐式、约定优于配置 ------ 框架替你做了大量决策,开发者只需关注业务逻辑
- React 18+ :显式、函数式、手动控制 ------ 框架只提供基础积木,开发者拥有完全控制权
本文将从响应式原理、虚拟 DOM、组件更新粒度、副作用管理、组合逻辑复用、编译时优化等核心技术维度,结合源码级分析和可视化图解,帮你彻底理解这两大框架的本质差异。
一、响应式原理:自动追踪 vs 不可变数据
1.1 Vue 3 --- Proxy 自动依赖收集
Vue 3 的响应式系统是整个框架的灵魂。它使用 ES6 的 Proxy 对象来拦截对数据的所有操作:
javascript
import { reactive, ref, computed, effect } from 'vue'
// reactive: 创建对象的响应式代理
const state = reactive({ count: 0, user: { name: 'Alice' } })
// ref: 创建基本类型的响应式引用
const count = ref(0)
// computed: 创建计算属性(带缓存)
const double = computed(() => count.value * 2)
// effect: 注册副作用函数(类似 watchEffect)
effect(() => {
console.log('count changed:', count.value)
})
源码级解析:
Vue 3 的响应式系统由三个核心函数构成:
javascript
// 简化版 reactive 实现
const reactiveMap = new WeakMap() // 缓存已创建的代理,避免重复代理
function reactive(target) {
// 1. 非对象直接返回
if (!isObject(target)) return target
// 2. 检查是否已有代理(解决代理对象再次被代理的问题)
if (reactiveMap.has(target)) {
return reactiveMap.get(target)
}
// 3. 创建 Proxy 代理
const proxy = new Proxy(target, mutableHandlers)
// 4. 缓存代理对象
reactiveMap.set(target, proxy)
return proxy
}
const mutableHandlers = {
get(target, key, receiver) {
// 收集依赖:将当前活跃的 effect 与这个属性关联
track(target, key)
const result = Reflect.get(target, key, receiver)
// 懒代理:只在访问时才递归创建代理,避免初始化时大量递归
if (isObject(result)) {
return reactive(result)
}
return result
},
set(target, key, value, receiver) {
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
// 值真正发生变化时才触发更新
if (oldValue !== value) {
trigger(target, key) // 触发所有依赖这个属性的 effect
}
return result
},
deleteProperty(target, key) {
const hadKey = Object.prototype.hasOwnProperty.call(target, key)
const result = Reflect.deleteProperty(target, key)
if (hadKey && result) {
trigger(target, key)
}
return result
}
}
关键设计亮点:
- 懒代理(Lazy Proxy):嵌套对象只在被访问时才创建 Proxy,避免初始化时的性能开销
- WeakMap 缓存:确保同一个对象只被代理一次,同时不阻止垃圾回收
- Reflect 的正确性 :使用
Reflect.get/set确保 getter/setter 中的this指向代理对象本身,而非原始对象
1.2 React 18 --- 不可变数据 + setState
React 的响应式模型与 Vue 完全不同。它不"监听"数据变化,而是通过显式调用 setState 来触发重新渲染:
javascript
import { useState, useReducer } from 'react'
function Counter() {
const [count, setCount] = useState(0)
const [user, setUser] = useState({ name: 'Alice', age: 25 })
const increment = () => {
setCount(count + 1) // 传入新值
}
const updateName = () => {
// 必须创建新对象!直接修改不会触发更新
setUser({ ...user, name: 'Bob' })
}
return <button onClick={increment}>{count}</button>
}
源码级解析:
React 的状态存储在 Fiber 节点的 memoizedState 链表中:
javascript
// 简化版 useState 实现
function useState(initialState) {
// 1. 获取当前正在渲染的 Fiber 节点
const fiber = currentlyRenderingFiber
// 2. 获取或创建 Hook 节点(按调用顺序索引)
const hook = mountWorkInProgressHook()
// 3. 处理更新队列
const queue = hook.queue
if (queue.pending !== null) {
// 有待处理的更新,计算新状态
const newState = reducer(hook.baseState, queue.pending.action)
hook.memoizedState = newState
}
// 4. 返回状态和 dispatch 函数
const dispatch = queue.dispatch
return [hook.memoizedState, dispatch]
}
// 当调用 setState 时
function dispatchSetState(fiber, queue, action) {
// 1. 创建更新对象
const update = {
lane: requestUpdateLane(fiber), // 确定优先级
action: action,
next: null
}
// 2. 加入更新队列(环形链表)
enqueueUpdate(fiber, queue, update)
// 3. 调度更新
scheduleUpdateOnFiber(fiber, lane)
}
关键设计亮点:
- 不可变数据 :状态更新必须创建新对象,React 用
Object.is比较引用 - Lane 优先级模型:React 18 用 Lane 替代了旧的 Expiration Time,实现更精确的优先级调度
- 自动批处理 :React 18 中,所有在事件回调中的
setState会自动合并为一次渲染
1.3 对比总结
| 特性 | Vue 3 Proxy | React 18 setState |
|---|---|---|
| 修改方式 | state.count++(直接修改) |
setCount(c => c + 1)(函数式更新) |
| 更新触发 | 自动追踪依赖 | 显式调用 setter |
| 嵌套对象 | 自动深层代理 | 需手动展开或使用 Immer |
| 数组操作 | arr.push() 自动响应 |
需 setArr([...arr, item]) |
| 心智负担 | 低(像操作普通对象) | 高(需理解不可变性) |
| 调试优势 | 直观 | 可时间旅行、可预测 |
二、虚拟 DOM 与 Diff 算法:编译优化 vs 运行时调度
2.1 Vue 3 的 Diff 策略
Vue 3 的 Diff 算法是**双端比较 + 最长递增子序列(LIS)**的组合:
双端比较算法详解:
javascript
// 简化版 Vue 3 diff 核心逻辑
function patchKeyedChildren(c1, c2, container) {
let i = 0
let e1 = c1.length - 1
let e2 = c2.length - 1
// 1. 从头开始比较(sync from start)
while (i <= e1 && i <= e2) {
if (isSameVNodeType(c1[i], c2[i])) {
patch(c1[i], c2[i], container)
i++
} else {
break
}
}
// 2. 从尾开始比较(sync from end)
while (i <= e1 && i <= e2) {
if (isSameVNodeType(c1[e1], c2[e2])) {
patch(c1[e1], c2[e2], container)
e1--
e2--
} else {
break
}
}
// 3. 处理新增或删除
if (i > e1) {
// 旧节点已遍历完,新增剩余新节点
while (i <= e2) {
patch(null, c2[i], container)
i++
}
} else if (i > e2) {
// 新节点已遍历完,删除剩余旧节点
while (i <= e1) {
unmount(c1[i])
i++
}
} else {
// 4. 中间乱序部分,使用 LIS 算法最小化移动
const s1 = i
const s2 = i
const keyToNewIndexMap = new Map()
// 建立新节点 key -> index 映射
for (i = s2; i <= e2; i++) {
const nextChild = c2[i]
keyToNewIndexMap.set(nextChild.key, i)
}
// 遍历旧节点,找到可复用的节点
const toBePatched = e2 - s2 + 1
const newIndexToOldIndexMap = new Array(toBePatched).fill(0)
for (i = s1; i <= e1; i++) {
const prevChild = c1[i]
const newIndex = keyToNewIndexMap.get(prevChild.key)
if (newIndex === undefined) {
unmount(prevChild) // 新列表中没有,删除
} else {
newIndexToOldIndexMap[newIndex - s2] = i + 1
patch(prevChild, c2[newIndex], container)
}
}
// 5. 使用最长递增子序列(LIS)确定不需要移动的节点
const increasingNewIndexSequence = getSequence(newIndexToOldIndexMap)
let j = increasingNewIndexSequence.length - 1
// 倒序遍历,确保插入位置正确
for (i = toBePatched - 1; i >= 0; i--) {
if (newIndexToOldIndexMap[i] === 0) {
// 全新节点,需要插入
patch(null, c2[s2 + i], container, anchor)
} else if (i !== increasingNewIndexSequence[j]) {
// 需要移动的节点
move(c2[s2 + i], container, anchor)
} else {
j--
}
}
}
}
最长递增子序列(LIS)的作用:
假设旧列表是 [A, B, C, D, E],新列表是 [A, C, B, D, F]:
- 双端比较后,确定
A和D的位置不变 - 中间乱序部分是
[B, C, E]→[C, B, F] - LIS 算法找到最长递增子序列
[C(位置1), D(位置3)](在新列表中的索引) - 这意味着
C和D不需要移动,只需移动B并新增F
时间复杂度:O(n) 的最佳情况(列表尾部增删),O(n log n) 的最坏情况(完全乱序)
2.2 React 18 的 Diff 策略
React 的 Diff 策略更简单但配合 Fiber 架构实现了独特的优势:
同层比较 + key 复用:
javascript
// React 的 reconcileChildren 简化版
function reconcileChildren(current, workInProgress, nextChildren) {
if (current === null) {
// 首次挂载,直接创建所有 Fiber
workInProgress.child = mountChildFibers(workInProgress, null, nextChildren)
} else {
// 更新阶段,diff 比较
workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren)
}
}
// 处理子节点数组
function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) {
let resultingFirstChild = null
let previousNewFiber = null
let oldFiber = currentFirstChild
let lastPlacedIndex = 0
let newIdx = 0
// 第一轮:按索引顺序比较,找到第一个不匹配的
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber
oldFiber = null
} else {
nextOldFiber = oldFiber.sibling
}
const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx])
if (newFiber === null) {
if (oldFiber === null) {
oldFiber = nextOldFiber
}
break
}
// key 相同但类型不同,删除旧节点
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
deleteChild(returnFiber, oldFiber)
}
}
// 记录位置变化
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx)
if (previousNewFiber === null) {
resultingFirstChild = newFiber
} else {
previousNewFiber.sibling = newFiber
}
previousNewFiber = newFiber
oldFiber = nextOldFiber
}
// 第二轮:处理剩余的新节点或旧节点
// ...
}
Fiber 架构的革命性设计:
React 16 引入的 Fiber 架构将渲染过程拆分为可中断的小任务:
javascript
// Fiber 节点的核心结构
interface Fiber {
type: any // 组件类型或 DOM 标签
key: string | null
stateNode: any // 对应的 DOM 节点或组件实例
// 树形结构
return: Fiber | null // 父节点
child: Fiber | null // 第一个子节点
sibling: Fiber | null // 下一个兄弟节点
// 工作单元
pendingProps: any
memoizedProps: any
memoizedState: any // Hooks 存储在这里
// 副作用
flags: Flags // 需要执行的 DOM 操作(Placement/Update/Deletion)
nextEffect: Fiber | null // 副作用链表
// 双缓冲
alternate: Fiber | null // 当前树和 workInProgress 树的对应节点
}
工作循环(Work Loop):
javascript
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
// shouldYield() 检查是否超过了时间切片(默认 5ms)
performUnitOfWork(workInProgress)
}
}
function performUnitOfWork(unitOfWork) {
// beginWork: 处理当前节点,返回下一个要处理的子节点
const next = beginWork(unitOfWork)
if (next === null) {
// 没有子节点了,完成当前节点
completeUnitOfWork(unitOfWork)
} else {
workInProgress = next
}
}
时间切片与优先级调度:
React 18 使用 scheduler 包实现任务调度:
javascript
import { startTransition, useDeferredValue } from 'react'
function App() {
const [input, setInput] = useState('')
const [list, setList] = useState([])
const handleChange = (e) => {
const value = e.target.value
setInput(value) // 高优先级:立即更新输入框
// 低优先级:大数据列表过滤,可中断
startTransition(() => {
setList(filterLargeList(value))
})
}
return (
<>
<input value={input} onChange={handleChange} />
<List items={list} />
</>
)
}
当用户输入时,输入框的更新是高优先级(InputDiscreteLane),而列表过滤是低优先级(TransitionLane)。如果用户在列表过滤过程中继续输入,React 会中断列表的渲染,优先处理输入框更新,确保交互不卡顿。
2.3 对比总结
| 特性 | Vue 3 | React 18 |
|---|---|---|
| Diff 策略 | 双端比较 + LIS | 同层比较 + key 复用 |
| 时间复杂度 | O(n) ~ O(n log n) | O(n) |
| 跨层级移动 | 可识别并复用 | 视为删除 + 创建 |
| 可中断渲染 | 不支持(借助 nextTick) | Fiber 原生支持 |
| 编译优化 | 静态提升、patchFlags | React 19 Compiler |
| 适用场景 | 列表重排频繁 | 超大型应用、交互密集 |
三、组件更新粒度:精确追踪 vs 子树重渲染
这是 Vue 和 React 最核心的差异之一,直接影响开发体验和性能优化策略。
3.1 Vue 3 --- 响应式驱动的精确更新
Vue 3 的组件更新是按需的、精确的:
vue
<script setup>
import { ref, reactive } from 'vue'
const state = reactive({
header: { title: 'My App' },
items: [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' }
]
})
const updateFirstItem = () => {
// 只修改第一个 item 的 name
state.items[0].name = 'Updated Item 1'
}
</script>
<template>
<div>
<Header :title="state.header.title" /> <!-- 不更新 -->
<Main>
<List>
<Item v-for="item in state.items" :key="item.id" :name="item.name" />
<!-- 只有第一个 Item 会重新渲染 -->
</List>
</Main>
<Footer /> <!-- 不更新 -->
</div>
</template>
原理:
- 组件渲染时会执行
render函数 render函数中访问响应式数据时,Vue 的track机制会将当前组件的 render effect 记录到这些数据的依赖集合中- 当数据变化时,
trigger机制只通知依赖它的 render effect 重新执行 - 没有访问过变化数据的组件,完全不会参与更新
javascript
// 简化版依赖收集
let activeEffect = null
function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, depsMap = new Map())
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, dep = new Set())
}
dep.add(activeEffect) // 将当前 effect 加入依赖集合
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => {
if (effect !== activeEffect) {
effect.run() // 只触发相关的 effect
}
})
}
}
3.2 React 18 --- setState 触发的子树重渲染
React 的默认行为是子树全部重渲染:
jsx
import { useState, memo, useMemo, useCallback } from 'react'
function App() {
const [items, setItems] = useState([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' }
])
const updateFirstItem = () => {
setItems(items.map((item, idx) =>
idx === 0 ? { ...item, name: 'Updated Item 1' } : item
))
}
return (
<div>
<Header /> {/* 默认会重渲染! */}
<Main>
<List items={items} />
{/* List 和所有 Item 默认都会重渲染 */}
</Main>
<Footer /> {/* 默认会重渲染! */}
</div>
)
}
// 必须手动优化
const Header = memo(function Header() {
return <header>My App</header>
})
const Item = memo(function Item({ name }) {
return <div>{name}</div>
})
为什么 React 选择这种设计?
React 团队认为"默认快速"不如"默认正确"。子树重渲染虽然看起来浪费,但确保了:
- 数据一致性:父组件更新后,子组件总能拿到最新的 props
- 可预测性:更新行为不依赖于隐式的依赖追踪,更容易理解
- 并发安全:在并发模式下,精确的依赖追踪可能导致不一致的中间状态
手动优化三板斧:
jsx
// 1. React.memo --- 浅比较 props
const MyComponent = memo(function MyComponent({ data, onClick }) {
return <div onClick={onClick}>{data}</div>
}, (prevProps, nextProps) => {
// 自定义比较函数(可选)
return prevProps.id === nextProps.id
})
// 2. useMemo --- 缓存计算结果
const processedData = useMemo(() => {
return heavyComputation(data)
}, [data])
// 3. useCallback --- 缓存函数引用
const handleClick = useCallback(() => {
doSomething(id)
}, [id])
3.3 React 19 Compiler --- 自动优化的新纪元
React 19 引入了 React Compiler,将 Vue 的"自动优化"理念引入 React:
jsx
// 之前:需要手动写 memo
const ExpensiveComponent = memo(function ExpensiveComponent({ data }) {
const processed = useMemo(() => process(data), [data])
return <div>{processed}</div>
})
// React 19 Compiler 后:无需手动优化
function ExpensiveComponent({ data }) {
const processed = process(data) // Compiler 自动插入缓存
return <div>{processed}</div>
}
Compiler 在构建阶段分析组件的依赖关系,自动插入 useMemo、useCallback 和 React.memo,性能提升 20%-40% ,同时减少 30%-50% 的样板代码 citeweb_search:2#6。
四、副作用管理:自动收集 vs 手动声明
4.1 Vue 3 --- watch / watchEffect
vue
<script setup>
import { ref, watch, watchEffect, onMounted, onUnmounted } from 'vue'
const count = ref(0)
const user = reactive({ name: 'Alice', age: 25 })
// watch: 明确指定监听源
watch(count, (newVal, oldVal) => {
console.log('count changed:', oldVal, '->', newVal)
})
// watch 多个源
watch([count, () => user.name], ([newCount, newName], [oldCount, oldName]) => {
console.log('multiple sources changed')
})
// watchEffect: 自动收集依赖,无需手动声明
watchEffect(() => {
// 访问了 count 和 user.name,两者任一变化都会触发
console.log('effect:', count.value, user.name)
})
// 生命周期钩子
onMounted(() => {
console.log('component mounted')
})
onUnmounted(() => {
console.log('component unmounted')
})
</script>
watch vs watchEffect 的区别:
| 特性 | watch | watchEffect |
|---|---|---|
| 依赖声明 | 显式指定 | 自动收集 |
| 首次执行 | 默认不执行(配置 immediate 可改) | 立即执行 |
| 旧值访问 | 可以 | 不可以 |
| 适用场景 | 特定数据变化时执行副作用 | 任意依赖变化时执行副作用 |
4.2 React 18 --- useEffect / useLayoutEffect
jsx
import { useState, useEffect, useLayoutEffect, useRef } from 'react'
function Example() {
const [count, setCount] = useState(0)
const [name, setName] = useState('Alice')
const divRef = useRef(null)
// useEffect: 异步执行,不阻塞渲染
useEffect(() => {
console.log('count or name changed:', count, name)
// 清理函数
return () => {
console.log('cleanup before next effect')
}
}, [count, name]) // 必须手动声明所有依赖!
// useLayoutEffect: 同步执行,在 DOM 变更后立即执行
useLayoutEffect(() => {
// 测量 DOM 尺寸,避免闪烁
const { width } = divRef.current.getBoundingClientRect()
divRef.current.style.height = `${width}px`
}, [])
return <div ref={divRef}>{count}</div>
}
useEffect 的执行时机:
渲染阶段(Render Phase) → Commit 阶段 → 浏览器绘制 → useEffect 执行
↓
useLayoutEffect 在此执行(同步)
依赖数组的陷阱:
jsx
// ❌ 错误:遗漏依赖
useEffect(() => {
console.log(count)
}, []) // ESLint 会报警告
// ❌ 错误:包含不必要的依赖导致频繁执行
useEffect(() => {
fetchData()
}, [fetchData]) // fetchData 每次渲染都是新引用
// ✅ 正确:使用 useCallback 稳定引用
const fetchData = useCallback(() => {
// ...
}, [])
useEffect(() => {
fetchData()
}, [fetchData])
4.3 对比总结
| 特性 | Vue watch/watchEffect | React useEffect |
|---|---|---|
| 依赖追踪 | 自动(watchEffect)/ 显式(watch) | 必须手动声明 |
| 执行时机 | 同步(nextTick 微任务) | 异步(绘制后) |
| 清理机制 | onCleanup 回调 | return 清理函数 |
| 闭包问题 | 无(数据始终最新) | 有(需 useRef 或依赖数组) |
| 心智负担 | 低 | 高(依赖数组易出错) |
五、组合逻辑复用:Composition API vs Hooks
5.1 Vue 3 --- Composition API
Composition API 是 Vue 3 引入的组合式 API,核心思想是将相关逻辑组织在一起:
javascript
// useMouse.js --- 可复用的组合式函数
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
// 在组件中使用
<script setup>
import { useMouse } from './useMouse'
import { useFetch } from './useFetch'
const { x, y } = useMouse()
const { data, loading, error } = useFetch('/api/user')
// 可以按逻辑组织代码,而非按选项类型
const { name, setName } = useUserName()
const { theme, toggleTheme } = useTheme()
</script>
Composition API 的优势:
- 无顺序限制 :可以在
setup中任意调用,不受条件/循环影响 - 无闭包陷阱:响应式数据始终是最新的,无需担心过期闭包
- 逻辑自包含:相关代码组织在一起,而非分散在 data、methods、computed 中
5.2 React --- Hooks
React Hooks 是 React 16.8 引入的,彻底改变了 React 的开发方式:
jsx
// useMouse.js
import { useState, useEffect } from 'react'
export function useMouse() {
const [position, setPosition] = useState({ x: 0, y: 0 })
useEffect(() => {
const update = (e) => setPosition({ x: e.pageX, y: e.pageY })
window.addEventListener('mousemove', update)
return () => window.removeEventListener('mousemove', update)
}, []) // 空依赖数组,只在挂载时执行
return position
}
// 在组件中使用
function App() {
const { x, y } = useMouse()
const { data, loading, error } = useFetch('/api/user')
// Hooks 必须按相同顺序调用!
const [count, setCount] = useState(0)
return (
<div>
Mouse: {x}, {y}
{loading ? 'Loading...' : data.name}
</div>
)
}
Hooks 的规则(Hook Rules):
- 只在最顶层调用:不能在循环、条件或嵌套函数中调用
- 只在 React 函数中调用:不能在普通 JavaScript 函数中调用
为什么有这些规则?
React 内部用链表存储 Hooks,靠调用顺序来定位:
javascript
// React 内部:Hooks 存储在 Fiber.memoizedState 中
// 每次渲染按顺序读取
const hook1 = fiber.memoizedState // useState
const hook2 = hook1.next // useEffect
const hook3 = hook2.next // useState
// 如果在条件中调用,顺序就会错乱!
// ❌ 错误
if (condition) {
const [state, setState] = useState(0) // Hook 顺序不稳定
}
闭包陷阱(Stale Closure):
jsx
function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
console.log(count) // 永远是 0!因为 effect 只在挂载时执行
setCount(count + 1) // 永远是 1!
}, 1000)
return () => clearInterval(timer)
}, []) // 空依赖数组导致闭包陷阱
// ✅ 修复方案 1:使用函数式更新
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1) // 使用最新状态
}, 1000)
return () => clearInterval(timer)
}, [])
// ✅ 修复方案 2:使用 useRef
const countRef = useRef(count)
countRef.current = count
return <div>{count}</div>
}
5.3 对比总结
| 特性 | Vue Composition API | React Hooks |
|---|---|---|
| 调用限制 | 无(任意位置调用) | 只能在顶层,不能条件调用 |
| 顺序依赖 | 无 | 严格按顺序索引 |
| 闭包问题 | 无(Proxy 代理始终最新) | 有(需函数式更新或 useRef) |
| 生命周期 | 显式钩子(onMounted 等) | useEffect 统一处理 |
| 代码组织 | 按逻辑组合 | 按副作用拆分 |
| 心智负担 | 低(接近普通 JS 函数) | 高(规则多、陷阱多) |
六、编译时优化:Vue 的模板编译 vs React 的 JSX
6.1 Vue 3 --- 模板静态分析
Vue 的模板在编译时会进行深度优化:
vue
<template>
<div>
<h1>Static Title</h1> <!-- 静态节点,编译时提升 -->
<p>{{ message }}</p> <!-- 动态文本,标记 TEXT -->
<div :class="cls"> <!-- 动态 class,标记 CLASS -->
<span>Static</span> <!-- 静态节点 -->
</div>
</div>
</template>
编译后的代码:
javascript
import { createElementVNode as _createElementVNode,
openBlock as _openBlock,
createElementBlock as _createElementBlock,
toDisplayString as _toDisplayString,
normalizeClass as _normalizeClass } from "vue"
// 静态节点提升到模块级别,只创建一次
const _hoisted_1 = /*#__PURE__*/_createElementVNode("h1", null, "Static Title", -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createElementVNode("span", null, "Static", -1 /* HOISTED */)
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
_hoisted_1, // 直接复用静态节点
_createElementVNode("p", null, _toDisplayString(_ctx.message), 1 /* TEXT */),
_createElementVNode("div", {
class: _normalizeClass(_ctx.cls) // 只比较 class
}, [
_hoisted_2 // 直接复用静态节点
], 2 /* CLASS */)
]))
}
patchFlags 标记系统:
javascript
// Vue 3 的 patchFlags
const PatchFlags = {
TEXT: 1, // 动态文本节点
CLASS: 2, // 动态 class
STYLE: 4, // 动态 style
PROPS: 8, // 动态 props(不包括 class/style)
FULL_PROPS: 16, // 具有动态 key 的 props
HYDRATE_EVENTS: 32,
STABLE_FRAGMENT: 64,
KEYED_FRAGMENT: 128,
UNKEYED_FRAGMENT: 256,
NEED_PATCH: 512,
DYNAMIC_SLOTS: 1024,
DEV_ROOT_FRAGMENT: 2048
}
块级优化(Block Tree):
Vue 3.2+ 引入了 Block Tree,将动态节点提取到数组中,diff 时直接遍历动态节点数组,跳过所有静态节点:
javascript
// 编译结果:动态节点被收集到数组中
const _dynamicChildren = [
_createElementVNode("p", null, _toDisplayString(_ctx.message)),
_createElementVNode("div", { class: _normalizeClass(_ctx.cls) })
]
// diff 时直接遍历 _dynamicChildren,跳过静态节点
6.2 React --- JSX 运行时 + React 19 Compiler
传统 React 中,JSX 几乎无编译时优化:
jsx
// JSX
function App() {
return (
<div>
<h1>Static Title</h1>
<p>{message}</p>
</div>
)
}
// 编译后(Babel 转换)
function App() {
return React.createElement('div', null,
React.createElement('h1', null, 'Static Title'),
React.createElement('p', null, message)
)
}
所有虚拟 DOM 都在运行时创建,无法预知哪些是静态的。
React 19 Compiler 的变革:
React Compiler 在构建阶段分析组件:
jsx
// 源代码
function ExpensiveComponent({ data, onSelect }) {
const processed = processData(data)
return <Item data={processed} onClick={onSelect} />
}
// Compiler 编译后(概念示意)
function ExpensiveComponent({ data, onSelect }) {
// 自动插入 useMemo
const processed = useMemo(() => processData(data), [data])
// 自动插入 useCallback
const memoizedOnSelect = useCallback(onSelect, [onSelect])
return <Item data={processed} onClick={memoizedOnSelect} />
}
性能收益:
- 减少不必要渲染 30%-70%
- 运行时性能提升 20%-40%
- 代码量减少 30%-50%(无需手动 memo)
6.3 对比总结
| 特性 | Vue 3 模板编译 | React JSX |
|---|---|---|
| 静态提升 | 原生支持 | React 19 Compiler |
| patchFlags | 标记动态属性类型 | 无(全量比较) |
| Block Tree | 动态节点数组 | 无 |
| 编译输出 | 优化后的渲染函数 | createElement 调用 |
| SSR 优化 | 编译优化同样生效 | 无编译优化 |
七、并发特性:Vue 的 nextTick vs React 的 Concurrent Mode
7.1 Vue 3 --- nextTick
Vue 3 没有原生并发渲染,但提供了 nextTick 来延迟 DOM 更新:
javascript
import { ref, nextTick } from 'vue'
const count = ref(0)
async function increment() {
count.value++
console.log(document.getElementById('counter').textContent) // 还是 0!
await nextTick()
console.log(document.getElementById('counter').textContent) // 现在是 1
}
Vue 的更新是同步收集、异步批量执行的。同一事件循环中的所有数据修改会合并为一次更新。
7.2 React 18 --- Concurrent Mode
React 18 的并发模式是框架层面的革命:
jsx
import { useState, useTransition, useDeferredValue, Suspense } from 'react'
function App() {
const [input, setInput] = useState('')
const [tab, setTab] = useState('home')
const [isPending, startTransition] = useTransition()
// useDeferredValue: 延迟更新非紧急值
const deferredInput = useDeferredValue(input)
const handleTabChange = (newTab) => {
// startTransition: 将更新标记为低优先级
startTransition(() => {
setTab(newTab)
})
}
return (
<div>
<input
value={input}
onChange={e => setInput(e.target.value)} // 高优先级
/>
<button onClick={() => handleTabChange('profile')}>
Profile
{isPending && <Spinner />} {/* 显示过渡状态 */}
</button>
{/* Suspense: 异步组件加载状态 */}
<Suspense fallback={<Loading />}>
<TabContent tab={tab} />
</Suspense>
</div>
)
}
Lane 优先级模型:
React 18 用 Lane(车道)替代了旧的 Expiration Time:
javascript
// Lane 优先级(从高到低)
const SyncLane = 0b0000000000000000000000000000001 // 同步,最高优先级
const InputDiscreteHydrationLane = 0b0000000000000000000000000000010
const InputDiscreteLane = 0b0000000000000000000000000000100 // 离散输入
const InputContinuousHydrationLane = 0b0000000000000000000000000001000
const InputContinuousLane = 0b0000000000000000000000000010000 // 连续输入
const DefaultHydrationLane = 0b0000000000000000000000000100000
const DefaultLane = 0b0000000000000000000000001000000 // 默认
const TransitionHydrationLane = 0b0000000000000000000000010000000
const TransitionLane1 = 0b0000000000000000000000100000000 // 过渡 1
const TransitionLane2 = 0b0000000000000000000001000000000 // 过渡 2
// ... 更多过渡车道
const IdleLane = 0b0100000000000000000000000000000 // 空闲,最低优先级
时间切片(Time Slicing):
javascript
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress)
}
if (workInProgress !== null) {
// 还有工作没完成,但时间片用完了
// 将控制权交还给浏览器,下次继续
return RootInProgress
}
}
function shouldYield() {
// 检查是否超过了时间切片(默认 5ms)
return getCurrentTime() >= deadline
}
八、TypeScript 集成
8.1 Vue 3
vue
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
// ref 自动推断类型
const count = ref(0) // Ref<number>
const message = ref('hello') // Ref<string>
// 泛型显式指定
const user = ref<{ name: string; age: number } | null>(null)
// reactive 类型推断
const state = reactive({
count: 0,
user: { name: 'Alice' }
}) // 类型自动推断
// computed
const double = computed(() => count.value * 2) // ComputedRef<number>
// 组件 props 类型定义
interface Props {
title: string
count?: number
}
const props = defineProps<Props>()
// 事件类型定义
const emit = defineEmits<{
update: [value: number]
submit: [data: FormData]
}>()
</script>
8.2 React
tsx
import { useState, useCallback, useEffect, FC } from 'react'
// 接口定义
interface User {
id: number
name: string
email: string
}
interface UserCardProps {
user: User
onSelect: (id: number) => void
className?: string
}
// 函数组件类型
const UserCard: FC<UserCardProps> = ({ user, onSelect, className }) => {
const [isHovered, setIsHovered] = useState(false)
const handleClick = useCallback(() => {
onSelect(user.id)
}, [onSelect, user.id])
return (
<div
className={className}
onClick={handleClick}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{user.name}
</div>
)
}
// 自定义 Hook 类型
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
fetch(url)
.then(res => res.json())
.then((data: T) => {
setData(data)
setLoading(false)
})
.catch(err => {
setError(err)
setLoading(false)
})
}, [url])
return { data, loading, error }
}
// 使用
const { data: user } = useFetch<User>('/api/user/1')
九、技术选型决策指南
选择 Vue 3 的场景
- 快速开发:团队需要快速交付,减少学习成本
- 中小型应用:不需要极端的并发优化
- 模板偏好:团队更习惯 HTML-like 的模板语法
- 渐进式采用:可以在现有项目中逐步引入
- 国内生态:国内 Vue 社区活跃,文档完善
选择 React 18+ 的场景
- 大型应用:需要极致的性能优化和可预测性
- 高交互场景:复杂的动画、拖拽、实时协作
- 跨平台:React Native 共享代码
- 招聘优势:全球范围内 React 开发者更多
- 生态规模:npm 包数量是 Vue 的 5 倍以上 citeweb_search:2#0
2026 年的新趋势
- React 19 Compiler:自动优化正在缩小与 Vue 的开发效率差距
- Vue Vapor Mode:无虚拟 DOM 的编译模式,性能接近原生
- Server Components:React 的 RSC 正在改变前端架构范式
十、总结:两条路线的本质
| 层面 | Vue 3 | React 18+ |
|---|---|---|
| 哲学 | 自动化、隐式、约定优于配置 | 显式、函数式、手动控制 |
| 优化策略 | 编译时 + 运行时自动细粒度更新 | 运行时调度 + 手动 memo 优化(Compiler 正在改变) |
| 学习路线 | 先学模板、指令,再深入响应式原理 | 先学 JSX、Hooks 规则,再学性能优化和并发 |
| 适合领域 | 快速开发、中小型应用、需要 DOM 操作直觉的项目 | 大型应用、需要极致交互体验、高度可定制项目 |
一个根本技术点:Vue 试图让框架替开发者做更多决定(依赖收集、更新范围、优化),而 React 选择把控制权交给开发者,只提供可组合的基元(setState、useEffect、memo)。这两条路线没有绝对优劣,取决于团队对"黑盒魔法"的接受程度以及对"显式控制"的偏好。
参考资源
- Vue 3 源码:https://github.com/vuejs/core
- React 源码:https://github.com/facebook/react
- React 19 Compiler 文档:https://react.dev/learn/react-compiler
- Vue 3 响应式系统 RFC:https://github.com/vuejs/rfcs/blob/master/active-rfcs/0040-script-setup.md