一、啥是组合式函数?为什么要学它?
Vue 3 的组合式 API(ref、computed、watch 等)让我们能把同一个功能相关的代码写在一起 ,不用再像选项式 API 那样分散在 data、methods、computed 里。
但这还只是组件内部的写法。如果多个组件都需要同样的功能(比如监听鼠标位置、发起网络请求、读写 localStorage),难道要把那段代码在每个组件里都写一遍吗?
当然不。我们可以把那坨带响应式的逻辑 单独抽出来,放进一个函数里,这个函数就是组合式函数 。通常约定,函数名以 use 开头,比如 useMouse、useFetch。
一句话总结: 组合式函数就是"能共享的、带状态的逻辑块"。
二、第一个案例:监听鼠标位置
需求:实时显示鼠标在页面上的坐标。这个功能可能在多个组件里用到,所以我们把它抽成 useMouse。
2.1 编写组合式函数 useMouse.js
javascript
// 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 在组件中使用
vue
<template>
<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.1 useWindowSize.js
javascript
// 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 组件中使用
vue
<template>
<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.1 useFetch.js
javascript
// 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 组件中使用
vue
<template>
<div>
<button @click="userId++">下一个用户(ID: {{ userId }})</button>
<p v-if="loading">加载中...</p>
<p v-else-if="error">出错了:{{ 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.1 useLocalStorage.js
javascript
// 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 组件中使用
vue
<template>
<div>
<p>你的名字:{{ name }}</p>
<input v-model="name" placeholder="输入名字,刷新页面还在" />
<button @click="name = null">清除名字</button>
</div>
</template>
<script setup>
import { useLocalStorage } from './useLocalStorage.js'
// 就像使用一个普通的 ref,但会自动存到 localStorage
const name = useLocalStorage('user-name', '小明')
</script>
效果: 输入名字后刷新页面,输入框里的值还在,因为已经存到了 localStorage 里。
六、案例五:防抖函数
防抖是前端常用优化手段:用户连续输入时不要立刻触发搜索,而是等停止输入一段时间后再执行。我们写一个 useDebounce。
6.1 useDebounce.js
javascript
// useDebounce.js
import { ref, watch } from 'vue'
// sourceRef 是要防抖的源数据(ref),delay 是延迟时间(毫秒)
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 组件中使用
vue
<template>
<div>
<input v-model="keyword" 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.js
javascript
// 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 组件中使用
vue
<template>
<div>
<h2>用户搜索</h2>
<input v-model="keyword" placeholder="输入用户名称" />
<p v-if="loading">搜索中...</p>
<ul v-else>
<li v-for="user in result" :key="user.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 的集成,或者你想听什么也可以点播!