1.Vue3 里的「use」到底是干啥的?
刚上手Vue3的同学,大概率会对「use」这个词犯迷糊:为啥很多代码里函数都叫useXXX?自己咋写这种use开头的逻辑?它和普通函数有啥区别?别慌,这篇文章把Vue3里use的用法、自定义实践、常见场景全拆明白,看完你也能玩转逻辑复用~
Vue3 本身没有“官方内置的 use 函数”,但**以 use 开头命名的函数**是社区和官方推荐的「自定义 Hooks」写法(基于 Composition API 的逻辑复用思路),简单说,它是**把组件中可复用的逻辑(比如请求数据、监听事件、状态管理)封装成独立函数**,让多个组件能共享这部分逻辑,同时和 Vue 的响应式、生命周期深度绑定。举个例子:做“鼠标位置跟踪”功能时,把“监听 mousemove、存储坐标、销毁事件”这些逻辑放到 useMouse.js
里,其他组件要跟踪鼠标时,直接 import { useMouse } from './hooks'
调用,就能拿到实时坐标——不用在每个组件重复写事件绑定和解绑代码,这就是复用的价值。
核心特点是:和 Vue 的响应式(ref/reactive
)、生命周期钩子(onMounted/onUnmounted
等)深度整合,复用的不仅是代码,更是“响应式状态 + 副作用逻辑”的组合。
自己咋写一个 use 开头的自定义 Hook?
写自定义 Hook 就像“把组件里的一段逻辑拆出来,做成可插拔的‘逻辑插件’”,以“跟踪鼠标位置”为例,手把手写一个 useMousePosition
:
// src/hooks/useMousePosition.js import { ref, onMounted, onUnmounted } from 'vue' export function useMousePosition() { // 1. 用 ref 定义响应式状态(存储鼠标坐标) const x = ref(0) const y = ref(0) // 2. 定义副作用逻辑(绑定/解绑事件) function handleMouseMove(e) { x.value = e.pageX y.value = e.pageY } onMounted(() => { window.addEventListener('mousemove', handleMouseMove) }) onUnmounted(() => { window.removeEventListener('mousemove', handleMouseMove) }) // 3. 返回要暴露给组件的状态 return { x, y } }
然后在组件中使用:
<script setup> import { useMousePosition } from '@/hooks/useMousePosition' const { x, y } = useMousePosition() </script> <template> <div>鼠标X:{{ x }},Y:{{ y }}</div> </template>
写 Hook 的关键思路:
- 状态必须用
ref/reactive
包裹,保证数据变化能触发组件更新; - 生命周期钩子(如
onMounted
onUnmounted
)要和逻辑绑定,确保组件销毁时清理副作用(比如事件解绑); - 返回值按需暴露,让组件只拿需要的状态/方法,保持封装性。
再拓展个场景:写 useFetch
封装请求逻辑(用 ref
存 loading
error
data
,onMounted
发请求),组件调用后直接用这些状态,代码会清爽很多。
自定义 Hook 和普通函数有啥区别?
很多同学疑惑:“直接写普通函数封装逻辑不行吗?” 还真不一样,核心差异在“和 Vue 生态的结合度”:
对比维度 | 自定义 Hook | 普通函数 |
---|---|---|
响应式关联 | 用 ref/reactive 包状态,和组件响应式连接 |
返回普通值/对象,修改后组件不更新 |
生命周期绑定 | 用 onMounted 等钩子,和组件生命周期同步 |
写生命周期钩子无效(没和组件关联) |
逻辑复用粒度 | 封装“状态 + 副作用 + 逻辑”的组合包 | 仅复用纯逻辑,状态需组件自己维护 |
举个反面例子:如果把 useMousePosition
写成普通函数,事件监听绑了但没在组件卸载时销毁,多个组件调用会导致事件重复绑定,页面卡顿甚至崩溃(内存泄漏),而 Hook 里的 onUnmounted
会自动和组件生命周期挂钩,组件销毁时自动清理。
实际项目中,use Hooks 能解决哪些场景?
从业务开发到工具封装,use Hooks 应用场景极广,挑几个高频场景拆解:
场景1:网络请求逻辑复用
每个页面都写“loading、error、发请求”的重复代码?用 useFetch
统一封装:
// src/hooks/useFetch.js import { ref, onMounted } from 'vue' import axios from 'axios' export function useFetch(url) { const data = ref(null) const loading = ref(true) const error = ref(null) onMounted(async () => { loading.value = true try { const res = await axios.get(url) data.value = res.data } catch (e) { error.value = e } finally { loading.value = false } }) return { data, loading, error } }
组件中使用:
<script setup> import { useFetch } from '@/hooks/useFetch' const { data, loading, error } = useFetch('https://api.example.com/data') </script> <template> <div v-if="loading">加载中...</div> <div v-else-if="error">请求失败:{{ error.message }}</div> <div v-else>数据:{{ data }}</div> </template>
好处是:所有请求的 loading、错误处理逻辑集中维护,新页面请求时直接调用,不用重复写 try/catch
和状态管理。
场景2:轻量状态管理(不用 Vuex/Pinia 也能共享逻辑)
比如用户信息的获取和更新,用 useUser
封装:
// src/hooks/useUser.js import { ref, computed } from 'vue' // 从 localStorage 取初始用户信息 const userInfo = ref(JSON.parse(localStorage.getItem('user')) || { name: '游客' }) export function useUser() { // 状态:用户信息 const user = userInfo // 计算属性:是否登录 const isLogin = computed(() => user.value.id) // 方法:更新用户信息 function updateUser(newInfo) { user.value = { ...user.value, ...newInfo } localStorage.setItem('user', JSON.stringify(user.value)) } return { user, isLogin, updateUser } }
组件 A 修改用户信息,组件 B 能实时拿到最新值——因为 user
是 ref
响应式数据,多个组件调用 useUser
拿到的是同一个 ref
的引用,天然实现“跨组件状态同步”(比 Vuex 轻量,适合中小项目)。
场景3:浏览器 API 封装(让组件更干净)
比如封装 localStorage
的读写,用 useLocalStorage
自动同步响应式状态:
// src/hooks/useLocalStorage.js import { ref, watch } from 'vue' export function useLocalStorage(key, initialValue) { // 从 localStorage 取数据,无则用初始值 const storedValue = localStorage.getItem(key) const value = ref(storedValue ? JSON.parse(storedValue) : initialValue) // 监听 value 变化,同步到 localStorage watch(value, (newVal) => { localStorage.setItem(key, JSON.stringify(newVal)) }, { deep: true }) // 复杂对象需深监听 return value }
组件中使用:
<script setup> import { useLocalStorage } from '@/hooks/useLocalStorage' const theme = useLocalStorage('app-theme', 'light') // 自动读写 localStorage </script> <template> <button @click="theme = theme === 'light' ? 'dark' : 'light'"> 切换主题:{{ theme }} </button> </template>
组件不用关心 localStorage
读写细节,逻辑全在 Hook 里,维护更省心。
场景4:交互与动画逻辑复用
比如做“可拖拽元素”功能,用 useDraggable
封装:
// src/hooks/useDraggable.js import { ref, onMounted, onUnmounted } from 'vue' export function useDraggable(targetRef) { // 接收元素的 ref const x = ref(0) const y = ref(0) let startX = 0, startY = 0 function handleMouseDown(e) { startX = e.pageX - x.value startY = e.pageY - y.value window.addEventListener('mousemove', handleMouseMove) window.addEventListener('mouseup', handleMouseUp) } function handleMouseMove(e) { x.value = e.pageX - startX y.value = e.pageY - startY } function handleMouseUp() { window.removeEventListener('mousemove', handleMouseMove) window.removeEventListener('mouseup', handleMouseUp) } onMounted(() => { targetRef.value.addEventListener('mousedown', handleMouseDown) }) onUnmounted(() => { targetRef.value.removeEventListener('mousedown', handleMouseDown) }) return { x, y } }
组件中结合 ref
使用:
<script setup> import { ref } from 'vue' import { useDraggable } from '@/hooks/useDraggable' const boxRef = ref(null) const { x, y } = useDraggable(boxRef) </script> <template> <div ref="boxRef" :style="{ position: 'fixed', left: x + 'px', top: y + 'px' }"> 拖拽我~ </div> </template>
不管多少个组件要做拖拽,都能复用 useDraggable
,不用重复写事件绑定逻辑,代码量直接减半。
用 use Hooks 时容易踩哪些坑?咋避免?
写 Hook 爽,但稍不注意就会掉坑,分享几个高频踩坑点和解决方案:
坑1:忘记清理副作用,导致内存泄漏
比如在 Hook 里加了 window.addEventListener
,但没在 onUnmounted
里 removeEventListener
——组件销毁后事件还在,重复触发会让页面越来越卡。
解决:所有绑定的事件、定时器、订阅等,必须在 onUnmounted
里销毁,像 useMousePosition
的例子,onMounted
绑定 mousemove
,onUnmounted
就解绑,确保组件销毁后逻辑也清理干净。
坑2:响应式状态“失效”,修改后组件不更新
Hook 里返回的是普通对象/值,而非 ref/reactive
包裹的:
// 错误示例:返回普通对象,组件拿不到更新 export function useBadHook() { let count = 0 function increment() { count++ } return { count, increment } // count 是普通变量,修改后组件不更新 }
解决:所有要让组件响应的状态,必须用 ref
或 reactive
包装,改成:
export function useGoodHook() { const count = ref(0) function increment() { count.value++ } return { count, increment } // count 是 ref,修改会触发更新 }
坑3:变量命名冲突,Hook 内部状态互相干扰
比如两个 Hook 里都定义了叫 x
的 ref
,组件同时调用时,若逻辑依赖全局状态,就会串数据。
解决:Hook 内部状态尽量用局部变量(ref/reactive
在函数内定义),避免依赖全局状态,若需跨 Hook 共享,考虑用 Pinia/Vuex,或专门的状态管理 Hook(如前面 useUser
的例子,用 ref
在模块内共享)。
坑4:过度封装,把简单逻辑搞复杂
比如一个组件里只用一次的逻辑,硬拆成 Hook,反而增加维护成本。
解决:只对“真正复用 2 次以上”的逻辑做 Hook 封装,如果是组件内私有逻辑,直接写在 setup
里更直观。
Vue3 的 use 和 React Hooks 有啥不一样?
很多从 React 转 Vue 的同学好奇这个问题,核心差异来自框架设计:
- 响应式原理:Vue 基于 Proxy 自动响应式,修改
ref.value
或reactive
对象属性会自动触发更新;React 基于“依赖数组 + 手动触发”(useState
/useEffect
的依赖),Vue 的 Hook 不用写依赖数组,更省心。 - 执行时机与关联:Vue 的 Hook 执行和组件实例强关联,生命周期钩子(
onMounted
等)自动绑定到当前组件;React 的 Hooks 按顺序调用,依赖调用顺序保持状态,容易因顺序错误导致 Bug。 - 闭包问题:React 的
useEffect
容易因闭包捕获旧值踩坑;Vue 的响应式自动追踪依赖,Hook 里的逻辑更接近“同步写法”,很少遇到闭包导致的状态滞后问题。
简单说:Vue 的 use Hooks 写起来更自由,不用像 React 那样小心翼翼处理依赖和顺序,对新手更友好~
项目里咋组织这些 use Hooks 更合理?
随着项目变大,Hook 越来越多,得有好的目录结构和规范,否则找代码像大海捞针,分享实用组织方式:
-
按功能分类建文件夹:在
src
下新建hooks
文件夹,再分子文件夹:network
:放useFetch
、useRequest
等请求相关 Hook;ui
:放useDraggable
、useModal
(弹窗逻辑)等 UI 交互 Hook;state
:放useUser
、useCart
(购物车状态)等状态管理 Hook;browser
:放useLocalStorage
、useResize
(监听窗口 resize)等浏览器 API Hook。
-
单个 Hook 一个文件:
useMousePosition.js
单独放,避免一个文件塞多个 Hook 导致混乱。 -
用 TypeScript 增强类型:给 Hook 的参数、返回值加类型定义(
useFetch
的url
限定为字符串,返回的data
加泛型),让 IDE 智能提示,减少 Bug。 -
写 README 说明用途:每个 Hook 文件夹或关键 Hook 文件里写简单注释,说明用途、用法、依赖,团队协作更高效。
举个目录结构例子:
src/
└── hooks/
├── network/
│ └── useFetch.js
├── ui/
│ └── useDraggable.js
├── state/
│ └── useUser.js
└── browser/
└── useLocalStorage.js
这样不管自己维护还是新人接手,找逻辑、改逻辑都清晰很多。
Vue3 里的 use 自定义 Hooks 是「逻辑复用的核武器」,能让重复代码消失,组件更聚焦业务,从理解概念、动手写简单 Hook,到应对复杂场景、避坑优化,一步步练下来,你会发现项目代码量少了,可维护性却高了,现在就挑个重复逻辑,试着拆成 use Hook,感受下复用的快乐吧~
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。