Vue 和 React 中的hook
一、为什么会有Hook
Vue 和 React 中的 "hook" 都是为了解决在组件中复用逻辑的问题。主要为了解决以下两个问题:
1.逻辑复用困难
在 Hook 出现之前,React 和 Vue 都有各自的代码复用方式,但都存在一些问题:
-
React:主要使用高阶组件(Higher-Order Components, HOCs)和渲染属性(Render Props)。这二种方式往往会导致组件嵌套层级过深,形成所谓的“Wrapper Hell”(包裹地狱),让代码变得难以阅读和维护。
-
Vue:主要使用混入(Mixins)。当一个组件使用了多个混入时,开发者很难判断某个属性或方法是来自哪个混入,容易发生命名冲突。同时,由于数据来源不清晰,代码的可维护性也会降低。
Hook 的出现提供了一种更优雅、更清晰的方式来封装复用状态的逻辑。将一组相关的状态、副作用和方法封装成一个自定义 Hook(React)或组合式函数(Vue),然后在多个组件中引用,避免了复杂的组件嵌套或命名冲突。
1.状态逻辑分散且难以组织
在传统的类组件(React)或选项式 API(Vue)中,一个功能的完整逻辑往往被拆分到不同的地方。
-
React 类组件:比如一个组件需要处理数据获取,相关的代码会分散在
componentDidMount、componentDidUpdate和componentWillUnmount等不同的生命周期方法中,使一个功能(比如数据订阅)的逻辑不集中。 -
Vue 选项式 API:和react类似,一个功能的逻辑会被分散到
data、methods、computed和mounted等不同的选项中。当组件变得越来越复杂时,开发者需要来回切换不同的代码块才能理解一个完整的功能,可读性大大降低。
Hook 解决了这个问题,它允许开发者将相关联的逻辑代码组织在一起。
-
React:通过
useState、useEffect等 Hook,你可以把与某个功能相关的状态和副作用代码写在一起。 -
Vue:通过组合式 API,你可以将同一功能的
ref、computed、watch和生命周期钩子集中在一个setup函数中,甚至提取到一个独立的组合式函数中。
这种方式让代码更易于复用和维护,提高了可读性,也让复杂组件的维护变得更简单。
二、React Hook
React Hook 是在 React 16.8 中引入的。在此之前,如果你需要在组件中使用状态(state)或生命周期方法,必须使用类组件。Hook 的出现打破了这一限制,让开发者可以在函数组件中使用这些功能。
-
核心思想: React 的函数组件每次渲染时都会重新执行。Hook 就像一个 "钩子",将状态和副作用(side effects)"钩住"在这次渲染和下一次渲染之间。
-
常用 Hook:
-
useState: 用于给函数组件添加状态。每次调用setState更新状态时,组件都会重新渲染。 -
useEffect: 用于处理副作用,例如数据请求、DOM 操作、订阅事件等。它在组件渲染后执行,可以根据依赖数组来控制何时重新执行。 -
useContext: 用于在组件树中共享数据,避免一层层地通过 props 传递。 -
useRef: 用于引用 DOM 元素或在组件多次渲染之间保持一个可变的值,且该值的改变不会引起组件重新渲染。 -
useCallback,useMemo: 用于性能优化,分别缓存函数和计算结果,避免不必要的重新创建和计算。
-
-
使用规则:
-
只能在顶层调用: Hook 必须在函数组件或自定义 Hook 的顶层调用,不能在循环、条件判断或嵌套函数中调用。这是为了保证每次渲染时 Hook 的调用顺序是固定的。
-
自定义 Hook: 可以将多个 Hook 组合成一个自定义 Hook,用来复用复杂的逻辑。
-
举个例子
假设我们要创建一个自定义 Hook 来追踪鼠标在屏幕上的位置:
// useMousePosition.js - 这是一个自定义 Hook import { useState, useEffect } from 'react'; function useMousePosition() { const [position, setPosition] = useState({ x: 0, y: 0 }); useEffect(() => { const handleMouseMove = (e) => { setPosition({ x: e.clientX, y: e.clientY }); }; window.addEventListener('mousemove', handleMouseMove); // 清理副作用 return () => { window.removeEventListener('mousemove', handleMouseMove); }; }, []); // 依赖数组为空,只在组件挂载时执行一次 return position; // 返回一个对象,包含 x 和 y } export default useMousePosition;
然后在一个函数组件中调用它:
// App.js - 这是一个函数组件 import useMousePosition from './useMousePosition'; function App() { const position = useMousePosition(); // 在函数组件中调用自定义 Hook return ( <div> <h1>The mouse is at:</h1> <p> x: {position.x}, y: {position.y} </p> </div> ); } export default App;
从上面的例子可以看出,useMousePosition 是一个独立的函数,它负责处理逻辑。而 App 才是一个函数组件,它调用 useMousePosition 来获取数据,并利用这些数据来渲染 UI。
三、Vue Composition API(Vue Hook)
Vue Composition API(组合式 API)是在 Vue 3 中正式推出的。它的出现是为了解决 Vue 2 中 Options API 在处理大型复杂组件时,代码分散、逻辑难以组织和复用的问题。
-
核心思想: Vue 的组合式 API 基于 Vue 的响应式系统。它允许你将同一功能的逻辑代码组织在一起,而不是像 Options API 那样把数据、方法、生命周期钩子等选项分散在不同地方。
-
常用 API:
-
ref,reactive: 用于创建响应式数据。ref主要用于基本数据类型,而reactive用于对象和数组。当数据改变时,所有使用到该数据的组件都会自动更新。 -
computed: 用于创建计算属性,其值会根据依赖的响应式数据自动更新。 -
watch,watchEffect: 用于监听响应式数据的变化并执行特定的回调函数。 -
生命周期钩子: 如
onMounted,onUpdated,onUnmounted等,用于在组件的不同生命周期阶段执行代码。
-
-
使用方式:
-
<script setup>: 这是 Vue 3 中使用组合式 API 的推荐方式,它简化了语法,让代码更简洁。 -
可组合性: 你可以轻松地将多个逻辑片段(称为 "Composable" 或 "组合式函数")提取到单独的文件中,并在多个组件中复用。这类似于 React 的自定义 Hook。
-
定义一个自定义hook:
// useCounter.js import { ref, computed } from 'vue'; export function useCounter() { const count = ref(0); const double = computed(() => count.value * 2); const increment = () => { count.value++; }; return { count, double, increment }; }
使用
// main.js import { useCounter } from './useCounter'; const { count, double, increment } = useCounter(); console.log(count.value); // 输出: 0 increment(); console.log(count.value); // 输出: 1 console.log(double.value); // 输出: 2
四、React Hook和Vue Composition API(Vue Hook)限制
1. React Hook 的限制
React Hook 的限制主要源于其依赖于调用顺序的设计。React 内部通过一个链表来存储每个 Hook 的状态,并根据 Hook 的调用顺序来匹配状态。因此,如果调用顺序改变,React 就无法正确地找到对应的状态,导致程序出错。
主要有以下两个核心规则:
-
只在 React 函数的顶层调用 Hook
-
限制:你不能在循环、条件语句(
if)、嵌套函数或回调函数中调用 Hooks。 -
原因:这保证了每次组件渲染时,Hook 的调用顺序是完全一致的。例如,如果
useState被包裹在if语句中,那么当条件不满足时,Hook 的调用顺序就会中断,导致后续的 Hook 状态错乱。 -
解决方案:如果需要在条件渲染中管理状态,应该在条件语句外部调用 Hook,然后根据条件来决定如何使用它们返回的值。
-
-
只在 React 函数组件或自定义 Hook 中调用 Hook
-
限制:你不能在普通的 JavaScript 函数或类组件中调用 Hook。
-
原因:Hook 依赖于 React 的上下文(即组件的渲染过程)来绑定状态和副作用。在普通函数中调用它们没有意义,因为没有组件可以关联这些状态。
-
解决方案:如果你想复用 Hook 的逻辑,应该将它封装在一个以
use开头的自定义 Hook 中,然后在函数组件里调用这个自定义 Hook。
-
2.Vue Composition API 的限制
Vue 组合式 API 的限制主要源于其需要正确的响应式上下文。它的核心是数据响应式,所有响应式数据、计算属性和监听器都需要在特定的上下文中创建。
主要有以下两个核心规则:
-
在
setup阶段调用 API-
限制:大多数组合式 API(如
ref,reactive,computed,watch等)必须在 Vue 组件的setup函数或<script setup>块中同步调用。 -
原因:Vue 内部在
setup阶段会为组件实例建立响应式上下文。在这个阶段创建的响应式数据才能被正确追踪,并且与组件的生命周期绑定。如果你在异步操作(如setTimeout或Promise的then回调)中调用它们,响应式上下文可能已经失效,导致 API 无法正常工作。 -
解决方案:将异步操作放在生命周期钩子(如
onMounted)或watch中,并在其内部处理数据更新,而不是在异步回调中创建新的响应式数据。
-
-
避免在
setup中使用this-
限制:在
setup函数中,this不再指向组件实例。 -
原因:在
setup执行时,组件实例还没有被创建和挂载,所以this是一个undefined。这是 Vue 3 的设计改变,目的是鼓励开发者使用组合式 API 来组织逻辑,而不是依赖于传统的this。 -
解决方案:如果你需要访问组件的 props、attrs、slots 或 emit 方法,它们会作为参数传递给
setup函数。所有在setup中声明的响应式数据和方法,都会被返回并在模板中直接使用,不再需要通过this来访问。
-
总结
尽管两者有一些相似的限制,但背后的原理有所不同:
-
React 的限制 是为了保持 Hook 调用顺序的一致性,这对于其基于单向数据流的渲染机制至关重要。
-
Vue 的限制 是为了确保响应式数据的正确绑定,所有响应式操作必须在组件的初始设置阶段完成。
五、什么是自定义Hook
Vue 和 React 的自定义 Hook(或 Vue 中的“组合式函数”)都是为了在函数式组件中封装和复用有状态的逻辑。自定义 Hook 本质上就是一个普通函数,但具体实现方式和设计理念存在一些关键差异。
React 的自定义 Hook
在 React 中,自定义 Hook 是一个以 use 开头的 JavaScript 函数。它内部可以调用其他内置 Hook(如 useState, useEffect),并返回数据和方法。
核心理念
React 的自定义 Hook 是一种逻辑共享机制,它将与 UI 无关的逻辑从组件中剥离出来。每次组件渲染时,调用自定义 Hook 就像是在组件内部“插入”了一段有状态的逻辑。
限制
-
必须在 React 函数组件或另一个自定义 Hook 的顶层调用。这是因为 React 依赖于 Hook 的调用顺序来正确匹配内部状态。
-
不能在普通 JavaScript 函数中使用。脱离了 React 的渲染上下文,Hook 无法工作。
Vue 的自定义 Hook (组合式函数)
在 Vue 中,通常称之为组合式函数 (Composable)。它同样是一个以 use 开头的 JavaScript 函数,用于封装可复用的状态逻辑。
核心理念
Vue 的组合式函数利用其响应式系统来创建和管理状态。它的核心是返回一个包含响应式数据和方法的对象,这些数据在任何使用它的组件中都会保持响应。
优点
-
与 Vue 组件松耦合。组合式函数本质上是响应式工具的集合,因此它们可以在任何普通的 JavaScript 函数中使用,不局限于 Vue 组件。
-
更直观的依赖追踪。Vue 的响应式系统会自动追踪依赖,开发者通常不需要手动维护依赖数组来防止重复执行,这在某些情况下简化了代码。
六、自定义Hook是否可以在普通函数中使用?
Vue 的组合式函数(Composables)可以在普通的 JavaScript 函数中使用,而 React 的自定义 Hook 不行。这是一个非常重要的区别,也是两种框架设计的体现。
为什么 Vue 可以?
Vue 的组合式函数本质上是封装了响应式逻辑的普通 JavaScript 函数。它的核心是 Vue 的响应式系统,而不是组件的渲染上下文。
因为 ref 和 computed 是独立的响应式工具,它们不依赖于 Vue 组件的特定渲染流程。它们只是创建了可追踪的数据,当数据变化时,会通知依赖它的地方。在这个例子中,通知的对象就是 console.log,但如果是在 Vue 组件中,通知的对象就是模板。
在普通函数中由于它没有被任何 Vue 模板“绑定”住,所以它的变化不会自动引发任何 UI 的重新渲染,也不能访问 Vue 框架提供的特定上下文(如路由、状态管理)。
考虑一个简单的 Vue 组合式函数:
// useCounter.js import { ref, computed } from 'vue'; export function useCounter() { const count = ref(0); const double = computed(() => count.value * 2); const increment = () => { count.value++; }; return { count, double, increment }; }
这个 useCounter 函数内部创建了响应式数据 count 和计算属性 double。当你在一个 Vue 组件的 <script setup> 中调用它时,Vue 知道将这些响应式数据与该组件实例关联起来。
但你也可以在普通 JavaScript 文件中调用它:
// main.js import { useCounter } from './useCounter'; const { count, double, increment } = useCounter(); console.log(count.value); // 输出: 0 increment(); console.log(count.value); // 输出: 1 console.log(double.value); // 输出: 2
为什么 React 不行?
React 的自定义 Hook 必须在 React 函数组件的顶层或另一个自定义 Hook 中调用。这是因为 React 的 Hook 依赖于 React 内部的渲染上下文和调用顺序。
React 自定义 Hook:
// useCounter.js import { useState } from 'react'; export function useCounter() { const [count, setCount] = useState(0); const increment = () => { setCount(c => c + 1); }; return { count, increment }; }
如果你在普通的 JavaScript 文件中调用这个 useCounter 函数,比如:
// main.js import { useCounter } from './useCounter'; const { count, increment } = useCounter(); // ❌ 错误!
这段代码会抛出错误,因为 useState 找不到与之关联的 React 组件。它依赖于 React 在组件渲染时建立的“内部状态链表”,如果脱离这个环境,useState 就会失去它的作用,无法分配和管理状态。
总结
这个区别揭示了 Vue 和 React 的根本差异:
-
Vue 的组合式 API 更像是一套独立的、可插拔的响应式工具库。这些工具可以被任何 JavaScript 环境使用,与组件框架本身是松耦合的,普通函数中只是会丢失自动触发 UI 的更新和获取组件上下文的功能。
-
React 的 Hook 是紧密集成在 React 渲染流程中的特性。它们是 React 组件模型的扩展,旨在解决类组件的痛点,因此它们必须依赖于 React 的渲染上下文才能工作。
简而言之,Vue 的 Composable 更多是独立逻辑上的封装,而 React 的 Custom Hook 是渲染上下文中的状态管理。

浙公网安备 33010602011771号