Skip to content

D09 · Composables vs React Hooks

对应主课: L37 Composition API 设计哲学 最后核对: 2026-04-01


1. 表面相似,底层不同

typescript
// Vue Composable
function useCounter() {
  const count = ref(0)
  const increment = () => count.value++
  return { count, increment }
}

// React Hook
function useCounter() {
  const [count, setCount] = useState(0)
  const increment = () => setCount(c => c + 1)
  return { count, increment }
}

看起来几乎一样,但执行模型完全不同。


2. 核心差异

维度Vue ComposableReact Hook
执行频率只执行 1 次(setup)每次渲染都重新执行
依赖追踪自动(Proxy)手动(deps 数组)
条件调用✅ 可以在 if/for 中❌ 必须在顶层
闭包陷阱不存在常见问题
返回值响应式 Ref快照值

3. 执行模型


4. 闭包陷阱

React 的闭包陷阱

javascript
function Timer() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    const id = setInterval(() => {
      console.log(count)  // ❌ 永远是 0!
      // 因为 useEffect 的回调闭包捕获了初始的 count 值
    }, 1000)
    return () => clearInterval(id)
  }, [])  // 空依赖 → 回调不会更新

  return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}

修复方式(繁琐):

javascript
// 方式 1: 添加依赖(但每次 count 变化都重建定时器)
useEffect(() => { /* ... */ }, [count])

// 方式 2: useRef(额外的心智负担)
const countRef = useRef(count)
countRef.current = count  // 每次渲染手动同步

Vue 没有这个问题

typescript
setup() {
  const count = ref(0)

  onMounted(() => {
    setInterval(() => {
      console.log(count.value)  // ✅ 永远是最新值
      // 因为 count 是 Ref 对象,.value 是 getter
      // 每次读取都是最新的
    }, 1000)
  })

  return { count }
}

5. 依赖声明

typescript
// React: 手动声明依赖数组
useEffect(() => {
  fetchData(userId, pageNum)
}, [userId, pageNum])  // ← 漏了一个就是 bug

useMemo(() => {
  return expensiveCalc(a, b)
}, [a, b])  // ← 忘了加依赖 → 缓存值过期

// Vue: 自动追踪依赖
watchEffect(() => {
  fetchData(userId.value, pageNum.value)
  // 自动追踪 userId 和 pageNum,不需要声明
})

const result = computed(() => {
  return expensiveCalc(a.value, b.value)
  // 自动追踪 a 和 b,不需要 deps 数组
})

6. 条件调用

typescript
// React: ❌ hooks 不能在条件语句中
function App({ isAdmin }) {
  if (isAdmin) {
    useAdminPanel()  // ❌ React 报错!
  }
}

// Vue: ✅ composable 可以在任意位置调用
setup() {
  if (props.isAdmin) {
    useAdminPanel()  // ✅ 完全合法
  }
}

因为 Vue 的 setup 只执行一次,不依赖调用顺序来匹配状态。React Hooks 必须保证每次渲染的调用顺序一致。

7. 实战对比:useMousePosition

同一个功能在两个框架中的实现差异:

typescript
// ===== Vue Composable =====
function useMousePosition() {
  const x = ref(0)
  const y = ref(0)

  function update(e: MouseEvent) {
    x.value = e.clientX
    y.value = e.clientY
  }

  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  return { x, y }
}
// setup 中调用一次 → 创建一次 → 永远工作
// update 函数不会被重新创建
javascript
// ===== React Hook =====
function useMousePosition() {
  const [pos, setPos] = useState({ x: 0, y: 0 })

  useEffect(() => {
    const update = (e) => setPos({ x: e.clientX, y: e.clientY })
    window.addEventListener('mousemove', update)
    return () => window.removeEventListener('mousemove', update)
  }, [])  // 空依赖 → 只绑定一次,但这里碰巧没问题

  return pos
}
// 每次渲染:函数组件重新执行 → useState 调用 → useEffect 检查依赖
// 返回的 pos 是一个新对象(但内容可能一样)

关键区别:

  • Vue 的 update 函数只创建一次,直接修改 ref
  • React 的每次渲染都重新执行 hook 函数(虽然 useEffect 内部有优化)
  • Vue 的返回值 { x, y } 是固定的 ref 引用,消费者不会因为鼠标移动而重渲染(除非模板用到了 x.value
  • React 的返回值 pos 每次 setPos 都是新对象,消费组件会重渲染

8. 性能影响

方面Vue ComposableReact Hook
函数执行1 次(setup)N 次(每次渲染)
闭包创建1 个(setup 闭包)N 个(每次渲染新回调)
GC 压力低(稳定引用)较高(每次创建新闭包)
优化手段无需优化useCallback / useMemo
Devtools 体验稳定的 ref 引用快照值,需要 Profiler

React 需要 useCallbackuseMemo 来优化性能,Vue 天然不需要这些。这不是 React 的缺陷,而是两种响应式模型的本质差异。


9. 总结

Vue ComposableReact Hook
心智模型响应式变量渲染快照
执行一次性设置每次渲染
陷阱几乎没有闭包/依赖声明
性能优化不需要useCallback/useMemo
学习曲线理解 Ref理解 Hooks 规则

两者都是优秀的逻辑复用方案,但 Vue 的自动依赖追踪让开发体验更顺畅,性能优化的心智负担更低。