在Vue 3的Composition API体系里,Hooks的核心价值是把组件中的响应式状态、衍生计算和生命周期副作用抽成可复用函数。原文围绕ref、reactive、computed、生命周期Hooks以及多个自定义Hook展开,重点不在概念介绍,而在代码组织和复用方式。下面按实现逻辑重新梳理这些Hook的写法、参数含义、调用关系和容易出错的细节。
一、响应式基础与衍生状态
ref用于基本类型,reactive用于对象,computed则基于已有响应式数据生成衍生值。原文给出的基础示例如下:
- import { ref, reactive } from 'vue'
- // ref 用于基本类型
- const count = ref(0)
- // reactive 用于对象
- const state = reactive({
- name: '张三',
- age: 25
- })
复制代码- import { ref, computed } from 'vue'
- const count = ref(0)
- const doubleCount = computed(() => count.value * 2)
复制代码
这里要注意ref和reactive在访问方式上的差异:ref的值通过.value读写,computed返回的也是ref形态的衍生状态。把这两类基础能力封装进自定义Hook时,返回值通常以对象形式暴露,调用方解构后即可在模板或逻辑中继续使用。
二、生命周期Hooks与副作用清理
Vue 3提供onMounted、onUpdated、onUnmounted等生命周期Hooks,用于在组件不同阶段执行逻辑。原文示例把三者集中在一个useLifecycleDemo函数中:
- import { onMounted, onUpdated, onUnmounted } from 'vue'
- export function useLifecycleDemo() {
- onMounted(() => {
- console.log('组件已挂载')
- })
- onUpdated(() => {
- console.log('组件已更新')
- })
- onUnmounted(() => {
- console.log('组件即将卸载')
- })
- }
复制代码
这个示例本身只做日志输出,但它体现了自定义Hook中处理生命周期的基本模式:在onMounted中注册监听或发起初始化,在onUnmounted中做对应清理。只要Hook内部注册了全局事件、定时器或订阅,就应在卸载阶段解除,否则组件反复创建时容易留下无效引用。
三、useCounter:可复用的计数器状态
useCounter是原文中最完整的同步状态Hook。它接收initialValue参数,默认值为0;内部用ref保存count,提供increment和decrement两个修改函数,并通过computed生成isPositive,用来判断当前值是否大于0。
- // useCounter.ts
- import { ref, computed } from 'vue'
- export function useCounter(initialValue = 0) {
- const count = ref(initialValue)
- function increment() {
- count.value++
- }
- function decrement() {
- count.value--
- }
- const isPositive = computed(() => count.value > 0)
- return {
- count,
- increment,
- decrement,
- isPositive
- }
- }
复制代码
从调用方式看,使用方只需要把useCounter的返回值解构出来,就能拿到count、increment、decrement和isPositive。这个Hook的设计点在于:初始值可配置,状态修改逻辑被封装在函数内部,衍生判断用computed而不是手动维护布尔变量,从而避免状态不同步。
四、useFetch:异步请求状态封装
异步逻辑是Hooks复用中更常见的场景。原文的useFetch接收url参数,内部维护data、error、loading三个ref,并定义fetchData执行fetch请求。请求成功时把响应JSON写入data并结束loading;请求失败时把错误写入error并结束loading。
- // useFetch.ts
- import { ref, computed } from 'vue'
- export function useFetch(url: string) {
- const data = ref(null)
- const error = ref(null)
- const loading = ref(true)
- async function fetchData() {
- try {
- const response = await fetch(url)
- data.value = await response.json()
- loading.value = false
- } catch (err) {
- error.value = err
- loading.value = false
- }
- }
- fetchData()
- return {
- data,
- error,
- loading
- }
- }
复制代码
这段代码的关键在于fetchData在Hook内部被立即调用,因此调用useFetch时请求会马上发出,返回对象只暴露data、error、loading三个状态。原文没有把fetchData作为返回值暴露出来,所以按当前实现,调用方不能通过返回值手动触发第二次请求;如果需要重试或按条件请求,就要额外调整返回结构。这是阅读这段代码时容易忽略的调用边界。另外,当前错误处理只覆盖fetch或json解析抛出的异常,响应状态码并未单独判断,这是原实现的处理范围。
五、useLocalStorage:把响应式状态同步到本地存储
useLocalStorage用于把某个key对应的本地存储值转换成响应式数据,并在值变化时写回localStorage。它接收key和initialValue两个参数:先读取localStorage.getItem(key),如果存在则JSON.parse,否则使用initialValue;随后用watch监听value,在deep为true的情况下把新值JSON.stringify后写入。
- import { ref, watch } from 'vue'
- export function useLocalStorage(key: string, initialValue: any) {
- const storedValue = localStorage.getItem(key)
- const value = ref(storedValue ? JSON.parse(storedValue) : initialValue)
- watch(value, (newValue) => {
- localStorage.setItem(key, JSON.stringify(newValue))
- }, { deep: true })
- return value
- }
复制代码
这个Hook的调用方式很直接:传入存储键和默认值,返回一个ref,调用方继续按ref的方式读写。需要留意两个错误来源:一是localStorage中已有内容不是合法JSON时,JSON.parse会抛异常,原文代码没有捕获;二是代码直接访问localStorage,在非浏览器环境或本地存储不可用的环境下会出现引用错误。使用前应确认运行环境满足前提,必要时在调用侧处理异常。
六、useMousePosition:全局事件监听与卸载清理
鼠标位置Hook展示了生命周期与DOM事件配合的典型写法。x和y分别用ref初始化为0,update函数从MouseEvent的pageX和pageY取值;onMounted时注册window的mousemove监听,onUnmounted时移除同一个update引用。
- import { ref, onMounted, onUnmounted } from 'vue'
- export function useMousePosition() {
- const x = ref(0)
- const y = ref(0)
- function update(event: MouseEvent) {
- x.value = event.pageX
- y.value = event.pageY
- }
- onMounted(() => {
- window.addEventListener('mousemove', update)
- })
- onUnmounted(() => {
- window.removeEventListener('mousemove', update)
- })
- return { x, y }
- }
复制代码
这里的重点是addEventListener和removeEventListener必须使用同一个函数引用,否则卸载时无法正确移除监听。Hook返回x、y两个ref,调用方可以直接在模板中读取坐标。由于监听挂在window上,如果组件频繁挂载和卸载而清理缺失,就会累积多个mousemove处理器,造成不必要的重复执行。
七、封装Hooks时的实践要点
结合原文示例,可以把Hooks的设计原则落到具体代码上:每个Hook只负责一件事,例如useCounter只管理计数,useMousePosition只管理鼠标坐标;命名统一以use开头,便于识别和检索;Hook内部尽量减少与具体组件耦合的副作用,事件监听和存储读写要在合适的生命周期内完成;异步Hook要明确loading、error、data三种状态的切换时机,并说明请求是立即执行还是由调用方触发;涉及localStorage、window等宿主环境对象时,要提前确认兼容性和异常处理边界。
总体来看,Vue 3 Hooks并不是独立于Composition API的新机制,而是把ref、reactive、computed和生命周期函数组合成可复用单元的一种代码组织方式。原文中的useCounter、useFetch、useLocalStorage和useMousePosition分别覆盖同步状态、异步请求、持久化存储和DOM事件四类场景,理解它们的参数、返回值和副作用清理方式,比单纯记忆写法更有实际价值。 |