首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Vue 3 和 React 的第四个差异:Composition API 和 Hooks 看起来像,实际不是一回事

Vue 3 和 React 的第四个差异:Composition API 和 Hooks 看起来像,实际不是一回事

原创
作者头像
peace-free
发布2026-06-09 11:21:20
发布2026-06-09 11:21:20
1230
举报
文章被收录于专栏:Vue3Vue3

Vue 3 推出 Composition API 之后,很多人会把它和 React Hooks 放在一起比较。这个比较有道理,因为它们都解决了一个老问题:组件里的逻辑如何复用。

在 Vue 2 时代,复杂逻辑常见方案包括 mixins、高阶组件式封装、renderless component。React 在 Hooks 出现之前,也有 mixins、HOC、render props。它们都能复用逻辑,但也都带来问题:来源不清、命名冲突、嵌套层级变深、类型推导困难、阅读成本高。

Composition API 和 Hooks 都让开发者可以写 useSomething() 这样的函数,把状态、副作用、订阅、事件监听封装起来。表面上看,两者很像。但如果认为 Vue 的组合式函数就是 React Hook 的 Vue 版本,就会误解两套框架的运行方式。

核心差异是:React Hooks 依赖组件函数反复执行时的调用顺序;Vue Composition API 依赖响应式系统建立的依赖关系。它们解决的问题相似,底层心智不同。

image-20260516094048783
image-20260516094048783

一、React Hooks:组件每次渲染都会重新执行

React 函数组件的基本模型是:给定 props 和 state,函数返回 UI。状态更新后,React 会再次调用组件函数,拿到下一份 JSX。Hooks 就运行在这个组件函数执行过程中。

例如:

代码语言:jsx
复制
function Counter() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    document.title = `count: ${count}`
  }, [count])

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

每次 count 更新,Counter 函数都会重新执行。useStateuseEffect 也会按顺序再次被调用。React 能把这一次的 useState 对应到上一次的 useState,靠的就是 Hook 调用顺序稳定。

这也是为什么 React 有 Hooks 规则:不能在条件语句、循环、嵌套函数里随便调用 Hook。因为一旦调用顺序变化,React 就无法正确匹配每个 Hook 对应的内部状态。

比如这种写法是错误方向:

代码语言:jsx
复制
if (enabled) {
  useEffect(() => {
    // ...
  }, [])
}

正确方式通常是把条件放进 Hook 内部:

代码语言:jsx
复制
useEffect(() => {
  if (!enabled) return
  // ...
}, [enabled])

Hooks 的好处是非常贴近 JavaScript 函数。你可以把逻辑封装成自定义 Hook,也可以组合多个 Hook。问题是你必须理解渲染快照、闭包、依赖数组和调用顺序,否则很容易遇到“为什么这里拿到的是旧值”“为什么 effect 一直执行”“为什么漏了依赖”的问题。

二、Vue Composition API:setup 建立响应式关系

Vue 3 的 Composition API 常见写法是:

代码语言:vue
复制
<script setup>
import { ref, computed } from 'vue'

const count = ref(0)
const double = computed(() => count.value * 2)

function add() {
  count.value++
}
</script>

<template>
  <button @click="add">{{ double }}</button>
</template>

<script setup> 会在组件实例创建时执行,里面创建 refreactivecomputedwatch,并注册生命周期。组件更新时,不是重新执行整个 <script setup>,而是响应式依赖变化后触发渲染更新。

这点和 React 很不一样。Vue 的组合式函数通常创建并返回响应式状态:

代码语言:js
复制
import { ref, onMounted, onUnmounted } from 'vue'

export function useWindowWidth() {
  const width = ref(window.innerWidth)

  function update() {
    width.value = window.innerWidth
  }

  onMounted(() => {
    window.addEventListener('resize', update)
  })

  onUnmounted(() => {
    window.removeEventListener('resize', update)
  })

  return { width }
}

组件使用它:

代码语言:vue
复制
<script setup>
import { useWindowWidth } from './useWindowWidth'

const { width } = useWindowWidth()
</script>

这个函数不是每次组件重新渲染都执行一遍。它在 setup 阶段建立响应式数据和生命周期关系。之后 width.value 变化,依赖它的模板或计算逻辑更新。

所以 Vue 没有 React 那种“Hook 必须保持调用顺序”的同类问题。Vue 也要求 provide、生命周期注册等 API 在 setup 阶段同步调用,但原因和 React 不完全一样。React 是为了稳定匹配 Hook 状态,Vue 是为了当前组件实例上下文和 effect 作用域能正确关联。

三、依赖数组:React 常见,Vue 较少显式写

React Hooks 中,useEffectuseMemouseCallback 都会遇到依赖数组:

代码语言:jsx
复制
useEffect(() => {
  fetchUser(userId)
}, [userId])

依赖数组表达的是:当这些值变化时,重新运行 effect。写少了可能读到旧值,写多了可能重复执行。React 官方也强调,不应该随意选择依赖,effect 里用到的响应式值通常都应该列出来。

Vue 里也有显式依赖,比如 watch(source, callback) 的 source。但 Vue 的 computedwatchEffect 会基于响应式读取自动追踪依赖:

代码语言:js
复制
const fullName = computed(() => `${firstName.value} ${lastName.value}`)

watchEffect(() => {
  console.log(fullName.value)
})

你在函数里读取了哪些响应式值,Vue 就能追踪。开发者不用手写依赖数组。这是 Vue 响应式系统带来的体验优势。

不过自动追踪也不是没有成本。它要求开发者理解“读取”才会建立依赖。如果你在异步边界之后读取,或者把响应式对象错误解构成普通值,依赖关系可能不是你以为的那样。React 的依赖数组啰嗦,但依赖写在明面上;Vue 的依赖追踪省心,但需要理解响应式边界。

四、闭包问题:React 更常见,Vue 不是没有

React Hooks 最常见的问题之一是闭包旧值。例如:

代码语言:jsx
复制
useEffect(() => {
  const timer = setInterval(() => {
    console.log(count)
  }, 1000)

  return () => clearInterval(timer)
}, [])

如果依赖数组是空的,定时器里的 count 会停留在创建 effect 那一轮渲染的值。这不是 React bug,而是 JavaScript 闭包和 React 快照模型共同导致的结果。

解决方式可能是把 count 加进依赖数组,也可能用函数式更新,也可能用 ref 保存最新值。具体取决于你要的行为。

Vue 中也有闭包,但因为响应式 ref 是一个稳定对象,读取 count.value 时通常能拿到当前值:

代码语言:js
复制
const count = ref(0)

setInterval(() => {
  console.log(count.value)
}, 1000)

这里闭包捕获的是 ref 对象,不是某一轮渲染里的 count 数值。这让很多场景更直观。

但这不代表 Vue 没有异步和旧值问题。如果你在某个时刻把 count.value 赋给普通变量,再在异步回调里使用这个普通变量,它仍然是旧值。框架不会改变 JavaScript 基本规则。只是 Vue 的响应式对象模型让一部分常见闭包问题不那么突出。

五、逻辑复用的组织方式不同

React 自定义 Hook 通常返回普通值、函数、对象:

代码语言:jsx
复制
function useSearch(keyword) {
  const [loading, setLoading] = useState(false)
  const [data, setData] = useState([])

  useEffect(() => {
    if (!keyword) return
    setLoading(true)
    fetch(`/api/search?q=${keyword}`)
      .then(res => res.json())
      .then(setData)
      .finally(() => setLoading(false))
  }, [keyword])

  return { loading, data }
}

每次组件渲染,这个 Hook 会被调用。Hook 内部的 state 由 React 保管,effect 根据依赖决定是否重新执行。

Vue 组合式函数通常返回 ref、computed、方法:

代码语言:js
复制
function useSearch(keyword) {
  const loading = ref(false)
  const data = ref([])

  watch(keyword, async value => {
    if (!value) return
    loading.value = true
    try {
      data.value = await fetch(`/api/search?q=${value}`).then(res => res.json())
    } finally {
      loading.value = false
    }
  })

  return { loading, data }
}

这里的 keyword 可以是 ref,watch 监听它的变化。逻辑建立后,响应式系统负责触发。

React 的复用更像“每次渲染重新运行一套函数逻辑,但内部状态被 Hook 系统保存”。Vue 的复用更像“在 setup 阶段创建一组响应式资源,之后由依赖变化驱动”。

六、哪种更好维护

如果团队熟悉 JavaScript 函数式思维,React Hooks 非常强。它把组件逻辑变成普通函数组合,抽象能力很强。尤其是配合 TypeScript,很多复杂业务逻辑可以封装得很清楚。

但 React Hooks 对基础要求也高。团队必须理解依赖数组、闭包快照、渲染次数、effect 清理、稳定引用。否则很容易出现隐蔽 bug。很多 React 项目的复杂性,并不是来自 JSX,而是来自 Hooks 写法不稳定。

Vue Composition API 对业务开发更友好。响应式对象、computed、watch、生命周期组合起来,逻辑拆分自然,和模板连接也直接。它避免了很多 React Hooks 中的依赖数组和闭包困扰。

但 Vue 也需要规范。组合式函数如果随意修改外部状态,或者返回一堆深层响应式对象,项目大了也会难追踪。尤其是多个组合式函数相互引用时,要注意副作用边界,不要让一个 useXxx 里面偷偷做太多事情。

七、实践建议

React 项目里,自定义 Hook 应该保持职责单一。一个 Hook 最好解决一个明确问题,比如请求、订阅、表单字段、权限判断。effect 的依赖不要靠猜,能用 ESLint 插件就用。需要读取最新值但不想触发渲染时,可以考虑 ref,但不要用 ref 逃避状态管理。遇到复杂状态变化时,useReducer 往往比多个 useState 更清晰。

Vue 项目里,组合式函数应该明确输入和输出。输入如果是响应式值,最好用命名说明清楚。返回值尽量稳定,不要让调用方猜哪些是 ref,哪些是普通值。对只读状态可以返回 readonly 包装,避免外部随意修改。副作用要配合生命周期清理,尤其是事件监听、定时器、WebSocket、第三方库实例。

八、结论:相似的是目标,不同的是运行模型

Composition API 和 Hooks 都让逻辑复用从“组件结构复用”转向“函数逻辑复用”。这是它们相似的地方。

但 React Hooks 建立在组件函数反复执行和状态快照模型上,必须重视调用顺序、依赖数组和闭包。Vue Composition API 建立在响应式系统和 setup 阶段上,更重视 ref、reactive、computed、watch 之间的依赖关系。

如果只看 useXxx() 命名,两者确实像。真正写项目时,它们完全不是同一套心智模型。理解这个差异,才能少踩很多坑。

代码补充:同一个倒计时逻辑的两种封装

Vue 组合式函数更像创建一组响应式资源:

代码语言:js
复制
import { computed, onUnmounted, ref } from 'vue'

export function useCountdown(seconds) {
  const left = ref(seconds)
  let timer = null

  const finished = computed(() => left.value <= 0)

  function start() {
    if (timer) return
    timer = setInterval(() => {
      left.value -= 1
      if (left.value <= 0) stop()
    }, 1000)
  }

  function stop() {
    clearInterval(timer)
    timer = null
  }

  onUnmounted(stop)

  return { left, finished, start, stop }
}

React 自定义 Hook 则要围绕 state、effect 和闭包边界设计:

代码语言:jsx
复制
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'

function useCountdown(seconds) {
  const [left, setLeft] = useState(seconds)
  const timerRef = useRef(null)

  const finished = useMemo(() => left <= 0, [left])

  const stop = useCallback(() => {
    clearInterval(timerRef.current)
    timerRef.current = null
  }, [])

  const start = useCallback(() => {
    if (timerRef.current) return
    timerRef.current = setInterval(() => {
      setLeft(value => {
        if (value <= 1) {
          clearInterval(timerRef.current)
          timerRef.current = null
          return 0
        }
        return value - 1
      })
    }, 1000)
  }, [])

  useEffect(() => stop, [stop])

  return { left, finished, start, stop }
}

这段对比能说明一个细节:Vue 里定时器回调读取和修改的是稳定的 ref;React 里更稳妥的方式是使用函数式更新,避免定时器闭包拿到旧 state。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、React Hooks:组件每次渲染都会重新执行
  • 二、Vue Composition API:setup 建立响应式关系
  • 三、依赖数组:React 常见,Vue 较少显式写
  • 四、闭包问题:React 更常见,Vue 不是没有
  • 五、逻辑复用的组织方式不同
  • 六、哪种更好维护
  • 七、实践建议
  • 八、结论:相似的是目标,不同的是运行模型
  • 代码补充:同一个倒计时逻辑的两种封装
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档