查看: 1144|回复: 3

Vue3自定义Hooks封装实战:状态复用与事件清理

[复制链接]
发表于 昨天 14:00 | 显示全部楼层 |阅读模式
在Vue 3的Composition API体系里,Hooks的核心价值是把组件中的响应式状态、衍生计算和生命周期副作用抽成可复用函数。原文围绕ref、reactive、computed、生命周期Hooks以及多个自定义Hook展开,重点不在概念介绍,而在代码组织和复用方式。下面按实现逻辑重新梳理这些Hook的写法、参数含义、调用关系和容易出错的细节。

一、响应式基础与衍生状态

ref用于基本类型,reactive用于对象,computed则基于已有响应式数据生成衍生值。原文给出的基础示例如下:
  1. import { ref, reactive } from 'vue'
  2. // ref 用于基本类型
  3. const count = ref(0)
  4. // reactive 用于对象
  5. const state = reactive({
  6.   name: '张三',
  7.   age: 25
  8. })
复制代码
  1. import { ref, computed } from 'vue'
  2. const count = ref(0)
  3. const doubleCount = computed(() => count.value * 2)
复制代码

这里要注意ref和reactive在访问方式上的差异:ref的值通过.value读写,computed返回的也是ref形态的衍生状态。把这两类基础能力封装进自定义Hook时,返回值通常以对象形式暴露,调用方解构后即可在模板或逻辑中继续使用。

二、生命周期Hooks与副作用清理

Vue 3提供onMounted、onUpdated、onUnmounted等生命周期Hooks,用于在组件不同阶段执行逻辑。原文示例把三者集中在一个useLifecycleDemo函数中:
  1. import { onMounted, onUpdated, onUnmounted } from 'vue'
  2. export function useLifecycleDemo() {
  3.   onMounted(() => {
  4.     console.log('组件已挂载')
  5.   })
  6.   onUpdated(() => {
  7.     console.log('组件已更新')
  8.   })
  9.   onUnmounted(() => {
  10.     console.log('组件即将卸载')
  11.   })
  12. }
复制代码

这个示例本身只做日志输出,但它体现了自定义Hook中处理生命周期的基本模式:在onMounted中注册监听或发起初始化,在onUnmounted中做对应清理。只要Hook内部注册了全局事件、定时器或订阅,就应在卸载阶段解除,否则组件反复创建时容易留下无效引用。

三、useCounter:可复用的计数器状态

useCounter是原文中最完整的同步状态Hook。它接收initialValue参数,默认值为0;内部用ref保存count,提供increment和decrement两个修改函数,并通过computed生成isPositive,用来判断当前值是否大于0。
  1. // useCounter.ts
  2. import { ref, computed } from 'vue'
  3. export function useCounter(initialValue = 0) {
  4.   const count = ref(initialValue)
  5.   function increment() {
  6.     count.value++
  7.   }
  8.   function decrement() {
  9.     count.value--
  10.   }
  11.   const isPositive = computed(() => count.value > 0)
  12.   return {
  13.     count,
  14.     increment,
  15.     decrement,
  16.     isPositive
  17.   }
  18. }
复制代码

从调用方式看,使用方只需要把useCounter的返回值解构出来,就能拿到count、increment、decrement和isPositive。这个Hook的设计点在于:初始值可配置,状态修改逻辑被封装在函数内部,衍生判断用computed而不是手动维护布尔变量,从而避免状态不同步。

四、useFetch:异步请求状态封装

异步逻辑是Hooks复用中更常见的场景。原文的useFetch接收url参数,内部维护data、error、loading三个ref,并定义fetchData执行fetch请求。请求成功时把响应JSON写入data并结束loading;请求失败时把错误写入error并结束loading。
  1. // useFetch.ts
  2. import { ref, computed } from 'vue'
  3. export function useFetch(url: string) {
  4.   const data = ref(null)
  5.   const error = ref(null)
  6.   const loading = ref(true)
  7.   async function fetchData() {
  8.     try {
  9.       const response = await fetch(url)
  10.       data.value = await response.json()
  11.       loading.value = false
  12.     } catch (err) {
  13.       error.value = err
  14.       loading.value = false
  15.     }
  16.   }
  17.   fetchData()
  18.   return {
  19.     data,
  20.     error,
  21.     loading
  22.   }
  23. }
复制代码

这段代码的关键在于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后写入。
  1. import { ref, watch } from 'vue'
  2. export function useLocalStorage(key: string, initialValue: any) {
  3.   const storedValue = localStorage.getItem(key)
  4.   const value = ref(storedValue ? JSON.parse(storedValue) : initialValue)
  5.   watch(value, (newValue) => {
  6.     localStorage.setItem(key, JSON.stringify(newValue))
  7.   }, { deep: true })
  8.   return value
  9. }
复制代码

这个Hook的调用方式很直接:传入存储键和默认值,返回一个ref,调用方继续按ref的方式读写。需要留意两个错误来源:一是localStorage中已有内容不是合法JSON时,JSON.parse会抛异常,原文代码没有捕获;二是代码直接访问localStorage,在非浏览器环境或本地存储不可用的环境下会出现引用错误。使用前应确认运行环境满足前提,必要时在调用侧处理异常。

六、useMousePosition:全局事件监听与卸载清理

鼠标位置Hook展示了生命周期与DOM事件配合的典型写法。x和y分别用ref初始化为0,update函数从MouseEvent的pageX和pageY取值;onMounted时注册window的mousemove监听,onUnmounted时移除同一个update引用。
  1. import { ref, onMounted, onUnmounted } from 'vue'
  2. export function useMousePosition() {
  3.   const x = ref(0)
  4.   const y = ref(0)
  5.   function update(event: MouseEvent) {
  6.     x.value = event.pageX
  7.     y.value = event.pageY
  8.   }
  9.   onMounted(() => {
  10.     window.addEventListener('mousemove', update)
  11.   })
  12.   onUnmounted(() => {
  13.     window.removeEventListener('mousemove', update)
  14.   })
  15.   return { x, y }
  16. }
复制代码

这里的重点是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事件四类场景,理解它们的参数、返回值和副作用清理方式,比单纯记忆写法更有实际价值。
回复

使用道具 举报

发表于 昨天 19:00 | 显示全部楼层

Re: Vue3自定义Hooks封装实战:状态复用与事件清理

看完收获挺多的,尤其是你把重点放在“代码怎么组织、怎么复用”而不是再讲一遍 API,这个视角对已经会用 ref 和 computed 的人更实用。 几个地方特别有共鸣。第一是 useCounter 里用 computed 生成 isPositive 而不是手动维护一个布尔变量,这个细节看着小,但实际项目里状态不同步的 bug 十有八九就是这么来的。第二是 useFetch 那段你点出的“调用边界”——fetchData 在 Hook 内部立即执行却没暴露出去,调用方只能拿到三个状态,想重试或者按条件请求就得改返回结构。这个提醒很到位,因为很多人看这类示例代码只注意 data/error/loading 的形状,不会去想“我能不能手动再发一次请求”。顺带说一句,按你描述的这个实现,组件卸载时如果请求还没回来,回来之后再去改 loading 和 data 其实已经没意义了,可以考虑加个取消机制;另外错误处理只覆盖了 fetch 和 json 解析抛出的异常,响应状态码这块也留了口子,你在文里也说明了这是原实现的范围,这点说清楚挺好的。 第三是生命周期那部分,你强调“注册了全局事件、定时器或订阅就要在卸载阶段解除”,这个思路其实可以再进一步,把注册和清理成对封装在同一个 Hook 里,比如事件监听那类,让调用方根本不用记着写清理,会更好用。 不过有一点得提一下,帖子好像被截断了,useL
回复 支持 反对

使用道具 举报

发表于 昨天 19:10 | 显示全部楼层

Re: Vue3自定义Hooks封装实战:状态复用与事件清理

整理得挺清楚的,尤其是把 ref 用 .value、computed 返回 ref 形态这些差异点单独拎出来,对刚上手 Composition API 的人很友好。useCounter 里用 computed 做正负判断而不是另外维护一个布尔值,这个思路值得强调,状态同步问题确实能少很多。 useFetch 那段你提到的调用边界很关键。如果 Hook 内部立即发起请求,又想支持重试或条件请求,比较稳妥的做法是把 fetchData 也一起返回,同时加一个 AbortController,在 onUnmounted 里 abort 掉未完成的请求,否则组件卸载后请求回来再写 data 容易产生警告或内存泄漏。另外响应状态码判断可以补一句 response.ok 的检查,把非 2xx 也归到 error 分支里,这样调用方处理起来更统一。 useLocalStorage 后面好像被截断了,watch 那部分没显示全。按你的描述用 deep: true 监听整体变化再序列化写回,常规场景没问题,但要注意两个细节:一是 initialValue 如果是引用类型,直接赋给 ref 后多个 Hook 实例可能共享同一个对象,最好用 structuredClone 或 JSON 深拷贝一下;二是 localStorage 在不同标签页之间不会自动同步,如果需要跨窗口一致,可以再监听 storag
回复 支持 反对

使用道具 举报

发表于 昨天 19:10 | 显示全部楼层

Re: Vue3自定义Hooks封装实战:状态复用与事件清理

梳理得挺清楚的,尤其是把“Hooks 的复用”落到参数、返回值和调用边界上,而不只是贴一段能跑的代码。几个点印象比较深: useCounter 里用 computed 派生 isPositive 而不是再维护一个布尔 ref,这个细节很关键。手动维护布尔值一旦忘了在 increment/decrement 里同步,状态就会漂移,用衍生值把这个问题从源头上消掉,确实是 Hook 该有的写法。 useFetch 那段指出的调用边界很到位。fetchData 在 Hook 内部立即执行,返回值只有 data/error/loading,调用方拿不到重试入口。如果实际业务里需要“搜索按钮触发重新请求”或者“换 URL 再拉一次”,这个设计就得改:要么把 fetchData 一起返回,要么把 url 做成 ref 或 getter 再配合 watch。另外 loading 初始化成 true 是对的,但如果后续允许手动重复调用,建议在 fetchData 开头先把它重置成 true,并把 error 清掉,否则第二次请求期间页面状态会和实际请求状态不一致。响应状态码那块你说得也准,当前只兜住了 fetch 和 json 解析的异常,非 2xx 的情况其实不会进 catch。 useLocalStorage 的思路是把“读一次、之后靠 watch 写回”这条链路固定下来,但这里有两个容易踩的坑
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-10-23 18:22 , Processed in 0.079218 second(s), 29 queries , Gzip On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部