Vue 3 与 React 18+ 核心技术深度对比:从源码到实战

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
  }
}

关键设计亮点:

  1. 懒代理(Lazy Proxy):嵌套对象只在被访问时才创建 Proxy,避免初始化时的性能开销
  2. WeakMap 缓存:确保同一个对象只被代理一次,同时不阻止垃圾回收
  3. 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)
}

关键设计亮点:

  1. 不可变数据 :状态更新必须创建新对象,React 用 Object.is 比较引用
  2. Lane 优先级模型:React 18 用 Lane 替代了旧的 Expiration Time,实现更精确的优先级调度
  3. 自动批处理 :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]

  1. 双端比较后,确定 AD 的位置不变
  2. 中间乱序部分是 [B, C, E][C, B, F]
  3. LIS 算法找到最长递增子序列 [C(位置1), D(位置3)](在新列表中的索引)
  4. 这意味着 CD 不需要移动,只需移动 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>

原理:

  1. 组件渲染时会执行 render 函数
  2. render 函数中访问响应式数据时,Vue 的 track 机制会将当前组件的 render effect 记录到这些数据的依赖集合中
  3. 当数据变化时,trigger 机制只通知依赖它的 render effect 重新执行
  4. 没有访问过变化数据的组件,完全不会参与更新
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 团队认为"默认快速"不如"默认正确"。子树重渲染虽然看起来浪费,但确保了:

  1. 数据一致性:父组件更新后,子组件总能拿到最新的 props
  2. 可预测性:更新行为不依赖于隐式的依赖追踪,更容易理解
  3. 并发安全:在并发模式下,精确的依赖追踪可能导致不一致的中间状态

手动优化三板斧:

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 在构建阶段分析组件的依赖关系,自动插入 useMemouseCallbackReact.memo,性能提升 20%-40% ,同时减少 30%-50% 的样板代码 citeweb_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 的优势:

  1. 无顺序限制 :可以在 setup 中任意调用,不受条件/循环影响
  2. 无闭包陷阱:响应式数据始终是最新的,无需担心过期闭包
  3. 逻辑自包含:相关代码组织在一起,而非分散在 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):

  1. 只在最顶层调用:不能在循环、条件或嵌套函数中调用
  2. 只在 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 的场景

  1. 快速开发:团队需要快速交付,减少学习成本
  2. 中小型应用:不需要极端的并发优化
  3. 模板偏好:团队更习惯 HTML-like 的模板语法
  4. 渐进式采用:可以在现有项目中逐步引入
  5. 国内生态:国内 Vue 社区活跃,文档完善

选择 React 18+ 的场景

  1. 大型应用:需要极致的性能优化和可预测性
  2. 高交互场景:复杂的动画、拖拽、实时协作
  3. 跨平台:React Native 共享代码
  4. 招聘优势:全球范围内 React 开发者更多
  5. 生态规模:npm 包数量是 Vue 的 5 倍以上 citeweb_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)。这两条路线没有绝对优劣,取决于团队对"黑盒魔法"的接受程度以及对"显式控制"的偏好。


参考资源

相关推荐
很晚很晚了5 小时前
纯前端转全栈 Day 1:我从第一个 NestJS 接口开始
前端
Lee川6 小时前
从零解剖一个 AI Agent Tool是如何实现的
前端·人工智能·后端
wangruofeng7 小时前
Playwright 深度调研:为什么它成了浏览器自动化的新底座
前端·测试
__log9 小时前
Vue 2 → Vue 3 迁移实战指南:不只是升级语法,更是一次思维跃迁
react.js
李白的天不白9 小时前
SSR服务端渲染
前端
卷帘依旧10 小时前
SSE(Server-Sent Events)完全指南
前端
码云之上10 小时前
万星入坞:我们如何用三层插件体系干掉巨石应用
前端·架构·前端框架
kyriewen11 小时前
一口气讲清楚 Monorepo、Turborepo、pnpm、Changesets 到底是什么?
前端·架构·前端工程化
IT_陈寒11 小时前
React性能优化踩的坑,这个错你可能也会犯
前端·人工智能·后端