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

原理:

  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 在构建阶段分析组件的依赖关系,自动插入 useMemo、useCallback 和 React.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)。这两条路线没有绝对优劣,取决于团队对"黑盒魔法"的接受程度以及对"显式控制"的偏好。


参考资源

相关推荐
liangshanbo12153 小时前
React Fiber:从“解决卡顿”到完整更新链路
前端·javascript·react.js
Timmy3 小时前
前端转全栈笔记:讲框架之前,先把 TypeScript 这关过了
前端·人工智能·node.js
计算机魔术师3 小时前
年收入650亿、亏420亿,Anthropic凭什么敢叫价万亿?
前端
Frag0ut3 小时前
Chrome Canary 最新实验功能:AI Mode、按设备筛选历史记录与 Gemini 自动改密
前端·人工智能·chrome·dev·beta·canary·最新实验功能
雪芽蓝域zzs3 小时前
第3节:el-select 弹窗定位、滚动穿透与挂载容器(popper-append-to-body /teleported)
vue.js·elementui
anew___4 小时前
《从零手写操作系统 (19):Ext2文件系统实战——从内存到磁盘的跨越》
java·服务器·前端·数据库·算法
吴声子夜歌4 小时前
Nginx应用与运维——Nginx Web服务应用实战(HTTPS安全服务器的搭建)
运维·前端·nginx
liangshanbo12154 小时前
Monorepo 工程化面试题:如何设计 Web + Node.js 中间层 + Shared + Scripts 的 Monorepo?
前端·node.js
IT_陈寒4 小时前
Redis集群切换主节点时,服务竟然全员掉线
前端·人工智能·后端
wangbing11254 小时前
开发指南147-WebSocket-前端
前端·websocket·网络协议