Vue组合式函数(Composables)从入门到实战:鼠标跟踪、请求封装、本地存储……全案例拆解
一、啥是组合式函数为什么要学它Vue 3 的组合式 APIref、computed、watch等让我们能把同一个功能相关的代码写在一起不用再像选项式 API 那样分散在data、methods、computed里。但这还只是组件内部的写法。如果多个组件都需要同样的功能比如监听鼠标位置、发起网络请求、读写 localStorage难道要把那段代码在每个组件里都写一遍吗当然不。我们可以把那坨带响应式的逻辑单独抽出来放进一个函数里这个函数就是组合式函数。通常约定函数名以use开头比如useMouse、useFetch。一句话总结组合式函数就是“能共享的、带状态的逻辑块”。二、第一个案例监听鼠标位置需求实时显示鼠标在页面上的坐标。这个功能可能在多个组件里用到所以我们把它抽成useMouse。2.1 编写组合式函数useMouse.jsjavascript// useMouse.js import { ref, onMounted, onBeforeUnmount } from vue // 定义一个组合式函数功能是跟踪鼠标位置 export function useMouse() { // 用 ref 定义坐标它们是响应式的 const x ref(0) // 横坐标 const y ref(0) // 纵坐标 // 鼠标移动时更新坐标 function handleMouseMove(event) { x.value event.pageX // pageX 是鼠标相对于整个文档的横坐标 y.value event.pageY // pageY 是纵坐标 } // 组件挂载后添加全局鼠标移动监听 onMounted(() { // addEventListener 给 window 绑定 mousemove 事件 window.addEventListener(mousemove, handleMouseMove) }) // 组件销毁前移除监听防止内存泄漏 onBeforeUnmount(() { window.removeEventListener(mousemove, handleMouseMove) }) // 把需要暴露给外部使用的响应式数据和方法返回出去 return { x, y } }关键点函数内部使用 Vue 的ref、onMounted等组合式 API就像在组件里一样。它返回一个对象包含了需要给外部用的响应式数据x和y。生命周期钩子在调用它的组件里生效当组件挂载时绑定事件销毁时自动移除。2.2 在组件中使用vuetemplate div p鼠标横坐标{{ mouseX }}/p p鼠标纵坐标{{ mouseY }}/p /div /template script setup // 引入刚才写的组合式函数 import { useMouse } from ./useMouse.js // 调用 useMouse拿到返回的 x 和 y // 这里用解构赋值并且可以重命名防止和其他变量冲突 const { x: mouseX, y: mouseY } useMouse() // 现在 mouseX 和 mouseY 就是响应式的模板里直接用 /script效果鼠标在页面上移动坐标实时更新。三、案例二监听窗口大小变化这次我们封装一个useWindowSize用于获取浏览器窗口的宽高窗口大小变化时自动更新。3.1useWindowSize.jsjavascript// useWindowSize.js import { ref, onMounted, onBeforeUnmount } from vue export function useWindowSize() { // 初始值设为 0挂载后更新为真实值 const width ref(0) const height ref(0) // 更新宽高的方法 function updateSize() { width.value window.innerWidth // 窗口宽度 height.value window.innerHeight // 窗口高度 } onMounted(() { // 先立即获取一次 updateSize() // 监听窗口大小变化事件 window.addEventListener(resize, updateSize) }) onBeforeUnmount(() { window.removeEventListener(resize, updateSize) }) return { width, height } }3.2 组件中使用vuetemplate div p窗口宽度{{ width }}px/p p窗口高度{{ height }}px/p /div /template script setup import { useWindowSize } from ./useWindowSize.js const { width, height } useWindowSize() /script四、案例三封装网络请求fetch大部分应用都要发请求。我们来写一个通用的useFetch接收一个 URL返回数据、加载状态和错误信息。4.1useFetch.jsjavascript// useFetch.js import { ref, watchEffect, toValue } from vue // urlOrFn 可以是一个字符串也可以是一个返回字符串的函数 export function useFetch(urlOrFn) { // 响应式数据存放请求回来的 JSON const data ref(null) // 是否正在加载 const loading ref(false) // 错误信息 const error ref(null) // watchEffect 会自动追踪依赖当 url 变化时重新请求 watchEffect(async () { // 重置状态 loading.value true data.value null error.value null // 如果是函数就调用获取 url如果是普通 ref/字符串 就用 toValue 取原始值 const url toValue(urlOrFn) try { const response await fetch(url) // 如果状态码不是 200-299抛出错误 if (!response.ok) { throw new Error(请求失败${response.status}) } data.value await response.json() } catch (err) { error.value err.message } finally { loading.value false } }) // 返回数据、加载状态、错误 return { data, loading, error } }4.2 组件中使用vuetemplate div button clickuserId下一个用户ID: {{ userId }}/button p v-ifloading加载中.../p p v-else-iferror出错了{{ error }}/p div v-else p用户名{{ data?.name }}/p p邮箱{{ data?.email }}/p /div /div /template script setup import { ref, computed } from vue import { useFetch } from ./useFetch.js const userId ref(1) // 根据 userId 动态生成请求 URL const url computed(() https://jsonplaceholder.typicode.com/users/${userId.value}) // 调用 useFetch传入 computed 生成的 url const { data, loading, error } useFetch(url) /script亮点useFetch内部用watchEffect自动追踪 URL 的变化URL 一变就重新请求。返回的data、loading、error都是响应式的模板直接用。五、案例四操作 localStorage很多场景需要把用户偏好比如主题、语言存到本地。我们写一个useLocalStorage让它能像ref一样读写并且自动同步到 localStorage。5.1useLocalStorage.jsjavascript// useLocalStorage.js import { ref, watch } from vue export function useLocalStorage(key, defaultValue) { // 尝试从 localStorage 读取没读到就用默认值 const storedValue localStorage.getItem(key) const initialValue storedValue ! null ? JSON.parse(storedValue) : defaultValue // 创建一个响应式 ref const data ref(initialValue) // 监听 data 的变化自动写回 localStorage watch(data, (newValue) { // 如果值为 null 或 undefined就删除该 key if (newValue null || newValue undefined) { localStorage.removeItem(key) } else { localStorage.setItem(key, JSON.stringify(newValue)) } }, { deep: true }) // deep: true 可以监听到对象内部的变化 return data }5.2 组件中使用vuetemplate div p你的名字{{ name }}/p input v-modelname placeholder输入名字刷新页面还在 / button clickname null清除名字/button /div /template script setup import { useLocalStorage } from ./useLocalStorage.js // 就像使用一个普通的 ref但会自动存到 localStorage const name useLocalStorage(user-name, 小明) /script效果输入名字后刷新页面输入框里的值还在因为已经存到了 localStorage 里。六、案例五防抖函数防抖是前端常用优化手段用户连续输入时不要立刻触发搜索而是等停止输入一段时间后再执行。我们写一个useDebounce。6.1useDebounce.jsjavascript// useDebounce.js import { ref, watch } from vue // sourceRef 是要防抖的源数据refdelay 是延迟时间毫秒 export function useDebounce(sourceRef, delay 500) { // 存放防抖后的值 const debouncedValue ref(sourceRef.value) let timer null // 监听源数据的变化 watch(sourceRef, (newValue) { // 每次变化先清除上一次的定时器 clearTimeout(timer) // 设置新的定时器 timer setTimeout(() { debouncedValue.value newValue }, delay) }) return debouncedValue }6.2 组件中使用vuetemplate div input v-modelkeyword placeholder输入搜索关键词 / p实时输入{{ keyword }}/p p防抖后500ms{{ debouncedKeyword }}/p /div /template script setup import { ref } from vue import { useDebounce } from ./useDebounce.js const keyword ref() const debouncedKeyword useDebounce(keyword, 500) // 后续发请求就可以用 debouncedKeyword 代替 keyword /script七、综合实战案例一个带防抖搜索的用户列表我们把前面学到的几个组合式函数揉在一起做一个完整的功能有一个搜索框用户输入关键词500ms 防抖后去请求一个模拟的 API展示过滤后的用户列表。7.1 模拟 API 函数为了方便演示先写一个模拟搜索的函数。javascript// api.js // 模拟用户数据 const users [ { id: 1, name: 小明 }, { id: 2, name: 小红 }, { id: 3, name: 小刚 }, { id: 4, name: 小丽 }, ] // 模拟异步搜索延迟 300ms 返回匹配的用户 export function searchUsers(keyword) { return new Promise((resolve) { setTimeout(() { const result users.filter(user user.name.includes(keyword)) resolve(result) }, 300) }) }7.2 自定义组合式函数useUserSearch.jsjavascript// useUserSearch.js import { ref, watch } from vue import { searchUsers } from ./api.js import { useDebounce } from ./useDebounce.js export function useUserSearch() { // 用户输入的关键词 const keyword ref() // 防抖后的关键词 const debouncedKeyword useDebounce(keyword, 500) // 搜索结果 const result ref([]) // 加载状态 const loading ref(false) // 当防抖后的关键词变化时执行搜索 watch(debouncedKeyword, async (newKeyword) { if (!newKeyword.trim()) { result.value [] return } loading.value true result.value await searchUsers(newKeyword) loading.value false }) return { keyword, result, loading } }7.3 组件中使用vuetemplate div h2用户搜索/h2 input v-modelkeyword placeholder输入用户名称 / p v-ifloading搜索中.../p ul v-else li v-foruser in result :keyuser.id{{ user.name }}/li /ul p v-if!loading keyword result.length 0无结果/p /div /template script setup import { useUserSearch } from ./useUserSearch.js const { keyword, result, loading } useUserSearch() /script代码拆解useUserSearch把搜索相关的所有逻辑防抖、请求、状态管理全部封装了起来。组件内部只需要调用它拿到keyword、result、loading然后纯展示。以后其他地方需要搜索功能直接复用useUserSearch就行代码完全不用重写。八、总结今天我们学习了组合式函数它是 Vue 3 复用逻辑的核心手段。编写套路导出一个以use开头的函数。函数内部可以使用ref、computed、watch、生命周期等所有组合式 API。返回需要给外部使用的响应式数据和方法。什么时候用多个组件需要同一段有状态的逻辑时。把组件里的复杂逻辑抽出来让组件更干净。常见命名useMouse、useFetch、useLocalStorage、useDebounce等。学会了组合式函数你就真正迈入了“中高级前端”的大门。以后维护项目、写新功能都会轻松不止一倍。有问题评论区说看到就回。下篇咱们可以聊聊Vue 与 TypeScript 的集成或者你想听什么也可以点播