
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 依赖响应式系统建立的依赖关系。它们解决的问题相似,底层心智不同。

React 函数组件的基本模型是:给定 props 和 state,函数返回 UI。状态更新后,React 会再次调用组件函数,拿到下一份 JSX。Hooks 就运行在这个组件函数执行过程中。
例如:
function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
document.title = `count: ${count}`
}, [count])
return <button onClick={() => setCount(count + 1)}>{count}</button>
}每次 count 更新,Counter 函数都会重新执行。useState、useEffect 也会按顺序再次被调用。React 能把这一次的 useState 对应到上一次的 useState,靠的就是 Hook 调用顺序稳定。
这也是为什么 React 有 Hooks 规则:不能在条件语句、循环、嵌套函数里随便调用 Hook。因为一旦调用顺序变化,React 就无法正确匹配每个 Hook 对应的内部状态。
比如这种写法是错误方向:
if (enabled) {
useEffect(() => {
// ...
}, [])
}正确方式通常是把条件放进 Hook 内部:
useEffect(() => {
if (!enabled) return
// ...
}, [enabled])Hooks 的好处是非常贴近 JavaScript 函数。你可以把逻辑封装成自定义 Hook,也可以组合多个 Hook。问题是你必须理解渲染快照、闭包、依赖数组和调用顺序,否则很容易遇到“为什么这里拿到的是旧值”“为什么 effect 一直执行”“为什么漏了依赖”的问题。
Vue 3 的 Composition API 常见写法是:
<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> 会在组件实例创建时执行,里面创建 ref、reactive、computed、watch,并注册生命周期。组件更新时,不是重新执行整个 <script setup>,而是响应式依赖变化后触发渲染更新。
这点和 React 很不一样。Vue 的组合式函数通常创建并返回响应式状态:
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 }
}组件使用它:
<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 Hooks 中,useEffect、useMemo、useCallback 都会遇到依赖数组:
useEffect(() => {
fetchUser(userId)
}, [userId])依赖数组表达的是:当这些值变化时,重新运行 effect。写少了可能读到旧值,写多了可能重复执行。React 官方也强调,不应该随意选择依赖,effect 里用到的响应式值通常都应该列出来。
Vue 里也有显式依赖,比如 watch(source, callback) 的 source。但 Vue 的 computed 和 watchEffect 会基于响应式读取自动追踪依赖:
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
watchEffect(() => {
console.log(fullName.value)
})你在函数里读取了哪些响应式值,Vue 就能追踪。开发者不用手写依赖数组。这是 Vue 响应式系统带来的体验优势。
不过自动追踪也不是没有成本。它要求开发者理解“读取”才会建立依赖。如果你在异步边界之后读取,或者把响应式对象错误解构成普通值,依赖关系可能不是你以为的那样。React 的依赖数组啰嗦,但依赖写在明面上;Vue 的依赖追踪省心,但需要理解响应式边界。
React Hooks 最常见的问题之一是闭包旧值。例如:
useEffect(() => {
const timer = setInterval(() => {
console.log(count)
}, 1000)
return () => clearInterval(timer)
}, [])如果依赖数组是空的,定时器里的 count 会停留在创建 effect 那一轮渲染的值。这不是 React bug,而是 JavaScript 闭包和 React 快照模型共同导致的结果。
解决方式可能是把 count 加进依赖数组,也可能用函数式更新,也可能用 ref 保存最新值。具体取决于你要的行为。
Vue 中也有闭包,但因为响应式 ref 是一个稳定对象,读取 count.value 时通常能拿到当前值:
const count = ref(0)
setInterval(() => {
console.log(count.value)
}, 1000)这里闭包捕获的是 ref 对象,不是某一轮渲染里的 count 数值。这让很多场景更直观。
但这不代表 Vue 没有异步和旧值问题。如果你在某个时刻把 count.value 赋给普通变量,再在异步回调里使用这个普通变量,它仍然是旧值。框架不会改变 JavaScript 基本规则。只是 Vue 的响应式对象模型让一部分常见闭包问题不那么突出。
React 自定义 Hook 通常返回普通值、函数、对象:
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、方法:
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 组合式函数更像创建一组响应式资源:
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 和闭包边界设计:
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 删除。