在 Vue3 的生态中,组合式 API 无疑非常重要,而组合式函数(Composables)则更加引人注目,今天我想和大家深入探讨组合式函数的魅力,看看它如何让我们的代码复用变得如此优雅。
从一个痛点开始:代码复用的困境
在前端开发中,我们经常会遇到这样的场景:多个组件需要实现相同的功能。比如,一个数据可视化组件需要跟踪鼠标位置,一个交互组件也需要跟踪鼠标位置。如果我们在每个组件里都写一遍相同的逻辑,不仅冗余,还会让后续维护变成噩梦。
在 Vue2 的 Options API 时代,我们可能会用混入(mixin)来解决这个问题,但混入带来的命名冲突、逻辑来源不清晰等问题,始终是开发者心中的一根刺。
而 Vue3 的组合式函数,正是为解决这类问题而生。
什么是组合式函数?
简单来说,组合式函数是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。
它和我们平时写的工具函数(比如格式化时间的函数)最大的区别在于:组合式函数管理的是有状态逻辑------ 也就是那些会随着时间变化的数据和相关操作。
用鼠标跟踪器理解组合式函数
让我们从一个简单的例子入手:跟踪鼠标位置。
如果我们直接在组件中实现这个功能,代码会是这样的:
xml
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
// 跟踪鼠标位置的状态
const x = ref(0)
const y = ref(0)
// 更新鼠标位置的方法
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
// 挂载时添加事件监听
onMounted(() => window.addEventListener('mousemove', update))
// 卸载时移除事件监听
onUnmounted(() => window.removeEventListener('mousemove', update))
</script>
<template>
鼠标位置:{{ x }}, {{ y }}
</template>
这段代码实现了功能,但如果另一个组件也需要这个功能,我们就得复制粘贴一遍。这显然不是我们想要的。
现在,我们把它改造成组合式函数:
javascript
// mouse.js
import { ref, onMounted, onUnmounted } from 'vue'
// 按照惯例,组合式函数以"use"开头
export function useMouse() {
// 封装内部状态
const x = ref(0)
const y = ref(0)
// 封装内部方法
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
// 封装生命周期逻辑
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
// 暴露状态
return { x, y }
}
然后在任何组件中使用它:
xml
<script setup>
import { useMouse } from './mouse.js'
// 只需一行代码,就能获得鼠标跟踪功能
const { x, y } = useMouse()
</script>
<template>
鼠标位置:{{ x }}, {{ y }}
</template>
是不是瞬间清爽了很多?这就是组合式函数的魔力 ------将复杂的有状态逻辑封装成可复用的函数,让组件代码更简洁,逻辑更清晰。
组合式函数的进阶技巧:处理响应式输入
组合式函数的威力不止于此。它还能轻松处理响应式输入,让逻辑复用更加灵活。
我们以一个数据请求的组合式函数useFetch
为例。首先实现一个基础版本:
javascript
// fetch.js
import { ref } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
// 发起请求
fetch(url)
.then(res => res.json())
.then(json => data.value = json)
.catch(err => error.value = err)
return { data, error }
}
这个版本的问题是:如果url
变化了,它不会重新发起请求。在实际开发中,我们经常需要根据响应式数据(比如路由参数、组件 props)的变化来重新请求数据。
如何让useFetch
支持响应式的url
呢?我们可以用watchEffect
和toValue
来实现:
javascript
// fetch.js
import { ref, watchEffect, toValue } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
watchEffect(() => {
// 重置状态
data.value = null
error.value = null
// toValue会处理各种类型的输入:
// - 如果是ref,取其.value
// - 如果是函数,执行并取返回值
// - 否则直接返回
fetch(toValue(url))
.then(res => res.json())
.then(json => data.value = json)
.catch(err => error.value = err)
})
return { data, error }
}
现在,useFetch
可以支持多种类型的url
参数:
- 静态字符串:
kotlin
const { data } = useFetch('/api/posts')
- 响应式 ref:
csharp
const url = ref('/api/posts')
const { data } = useFetch(url)
// 当url变化时,会自动重新请求
url.value = '/api/comments'
- getter 函数(常用于依赖其他响应式数据):
javascript
// 当props.id变化时,自动重新请求
const { data } = useFetch(() => `/api/posts/${props.id}`)
toValue
是 Vue3.3 新增的 API,它完美解决了组合式函数接收不同类型参数的问题,让函数的使用更加灵活。而watchEffect
则确保了在依赖变化时,逻辑会重新执行。
要写出高质量的组合式函数,有一些约定和最佳实践需要遵循:
命名规范
组合式函数应该以use
开头,采用驼峰命名法,比如useMouse
、useFetch
、useLocalStorage
。这样一眼就能看出这是一个组合式函数,方便识别和使用。
输入参数处理
尽量让组合式函数能够处理各种类型的输入参数(原始值、ref、getter),可以使用toValue
来统一处理:
javascript
import { toValue } from 'vue'
function useFeature(maybeRefOrGetter) {
const value = toValue(maybeRefOrGetter)
// ...
}
副作用管理
如果组合式函数中包含副作用(如事件监听、定时器、请求等),一定要在合适的时机清理它们,避免内存泄漏:
scss
function useTimer() {
const count = ref(0)
const timer = ref(null)
function start() {
timer.value = setInterval(() => {
count.value++
}, 1000)
}
function stop() {
clearInterval(timer.value)
}
// 组件卸载时停止定时器
onUnmounted(stop)
return { count, start, stop }
}
使用限制
组合式函数只能在<script setup>
或setup()
钩子中调用,并且只能同步调用。在某些情况下,也可以在生命周期钩子(如onMounted
)中调用。
这是因为组合式函数需要访问当前活跃的组件实例,才能正确注册生命周期钩子和响应式依赖。
总结
组合式函数是 Vue3 组合式 API 的精华所在,它让我们能够像搭积木一样组合和复用逻辑,极大地提升了代码的可维护性和复用性。
通过封装有状态逻辑,组合式函数让我们的组件变得更简洁,逻辑更清晰。它解决了 Vue2 中 mixin 带来的各种问题,为大型应用的开发提供了更优雅的解决方案。
欢迎在评论区分享你在使用组合式函数时的心得和技巧,让我们一起进步!