Vue3自定义组合式函数(Composable)通过函数封装实现逻辑复用,相比Vue2的Mixins具有显著优势。
文章展示了useMouse和useFetch两个典型示例,演示如何封装鼠标位置追踪和数据请求逻辑。
关键优势包括:
避免命名冲突,通过解构赋值可重命名;
来源清晰,每个状态和方法都有明确出处;
支持灵活组合,可条件使用;
提供更好的TypeScript支持。
Composable将相关逻辑集中管理,解决了Mixins的隐式依赖和代码分散问题,成为Vue3推荐的逻辑复用方案。
Vue3 举例说明如何编写一个自定义组合式函数 (Composable),例如 useMouse, useFetch,并理解其与 Mixins 相比的优势(命名冲突、来源清晰)
Vue3 自定义组合式函数 (Composable)
1. useMouse - 监听鼠标位置
html
<!-- useMouse.js -->
import { ref, onMounted, onUnmounted } from 'vue'
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 }
}
2. useFetch - 数据请求
html
<!-- useFetch.js -->
import { ref, watchEffect, toValue } from 'vue'
export function useFetch(url, options = {}) {
const data = ref(null)
const error = ref(null)
const loading = ref(false)
async function execute() {
loading.value = true
error.value = null
try {
const response = await fetch(toValue(url), toValue(options))
if (!response.ok) throw new Error('请求失败')
data.value = await response.json()
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
// 立即执行
watchEffect(() => {
if (toValue(url)) execute()
})
return { data, error, loading, execute, refresh: execute }
}
3. 在组件中使用
html
<!-- MouseTracker.vue -->
<template>
<div>
<h2>鼠标追踪器</h2>
<p>鼠标位置: X={{ x }}, Y={{ y }}</p>
<h2>用户数据</h2>
<div v-if="loading">加载中...</div>
<div v-else-if="error">错误: {{ error }}</div>
<pre v-else>{{ data }}</pre>
<button @click="refresh">刷新</button>
</div>
</template>
<script setup>
import { useMouse } from './composables/useMouse'
import { useFetch } from './composables/useFetch'
// 使用鼠标追踪 - 完全独立的状态
const { x, y } = useMouse()
// 使用数据请求 - 完全独立的状态
const { data, error, loading, refresh } = useFetch(
'https://jsonplaceholder.typicode.com/users/1'
)
// 可以使用多个 useFetch,互不干扰
const { data: posts } = useFetch(
'https://jsonplaceholder.typicode.com/posts'
)
</script>
4. 组合多个 Composable
html
<!-- AdvancedExample.vue -->
<script setup>
import { computed } from 'vue'
import { useMouse } from './composables/useMouse'
import { useFetch } from './composables/useFetch'
import { useLocalStorage } from './composables/useLocalStorage'
// 组合多个 composables
const { x, y } = useMouse()
const { data: weather } = useFetch(
computed(() => `https://api.weather.com/${x.value}/${y.value}`)
)
const { value: settings } = useLocalStorage('app-settings', { theme: 'dark' })
// 基于多个来源创建新状态
const mouseInfo = computed(() => `鼠标位置: (${x.value}, ${y.value})`)
</script>
Composable vs Mixins 优势对比
Mixins 的问题示例
javascript
// ❌ Mixins 的问题
// mixin1.js
export const mouseMixin = {
data() {
return {
x: 0,
y: 0
}
},
mounted() {
window.addEventListener('mousemove', this.update)
},
methods: {
update(event) {
this.x = event.pageX
this.y = event.pageY
}
}
}
// mixin2.js
export const fetchMixin = {
data() {
return {
data: null,
loading: false,
error: null,
x: '冲突的数据' // 命名冲突!
}
}
}
// 组件中使用
export default {
mixins: [mouseMixin, fetchMixin], // 冲突:x 被覆盖
mounted() {
console.log(this.x) // 哪个 x?不清楚!
// 难以追踪数据来源
}
}
Composable 的优势对比表
| 方面 | Mixins | Composable |
|---|---|---|
| 命名冲突 | ❌ 所有属性和方法合并到同一个实例,容易冲突 | ✅ 通过解构赋值,完全避免冲突 |
| 来源清晰 | ❌ 不清楚属性来自哪个 mixin | ✅ 明确知道每个状态的来源 |
| 逻辑复用 | ⚠️ 只能合并到组件选项,难以组合 | ✅ 函数式组合,灵活组合多个 composable |
| 类型推导 | ❌ TypeScript 支持差 | ✅ 完美的类型推导 |
| 隐式依赖 | ❌ 混入之间可能有隐式依赖 | ✅ 显式传递参数,依赖清晰 |
| 代码组织 | ❌ 逻辑分散在不同的选项区域 | ✅ 相关逻辑集中在一起 |
| Tree Shaking | ❌ 无法进行 tree shaking | ✅ 支持 tree shaking |
实际对比示例
html
<!-- ✅ 使用 Composable - 清晰、无冲突 -->
<script setup>
import { useMouse } from './composables/useMouse'
import { useFetch } from './composables/useFetch'
import { useCounter } from './composables/useCounter'
// 来源清晰,可以重命名避免冲突
const { x: mouseX, y: mouseY } = useMouse()
const { data: userData } = useFetch('/api/user')
const { data: productData } = useFetch('/api/products')
const { count, increment } = useCounter()
// 可以基于它们创建派生状态
const totalClicks = computed(() => count.value * 2)
</script>
<!-- ❌ 使用 Mixins - 混乱、易冲突 -->
<script>
import mouseMixin from './mixins/mouse'
import fetchMixin from './mixins/fetch'
import counterMixin from './mixins/counter'
export default {
mixins: [mouseMixin, fetchMixin, counterMixin],
computed: {
// 这里的 x、data、count 来自哪里?不清楚!
// 如果多个 mixin 有同名属性,会发生覆盖!
totalClicks() {
return this.count * 2 // count 来自哪个 mixin?
}
}
}
</script>
高级特性:Composable 的额外优势
javascript
// ✅ Composable 支持响应式参数
export function useCustomFetch(url, options = {}) {
const data = ref(null)
// 可以响应式地重新请求
watchEffect(() => {
const currentUrl = toValue(url)
if (currentUrl) {
fetch(currentUrl).then(res => res.json()).then(data.value = res)
}
})
return { data }
}
// ✅ Composable 可以条件性使用
const shouldTrack = ref(false)
const { x, y } = shouldTrack.value ? useMouse() : { x: ref(0), y: ref(0) }
// ✅ Composable 可以相互组合
export function useUserProfile(userId) {
const { data: user } = useFetch(`/api/users/${userId}`)
const { data: posts } = useFetch(`/api/users/${userId}/posts`)
return { user, posts }
}
总结
Composable 的核心优势:
-
清晰的来源 - 每个状态和方法都有明确的来源函数
-
无命名冲突 - 通过解构赋值可以任意重命名
-
灵活组合 - 函数式组合比对象合并更灵活
-
更好的类型支持 - 完美的 TypeScript 支持
-
逻辑聚焦 - 相关代码组织在一起,更易维护
这些特性使 Composable 成为 Vue3 中逻辑复用的最佳实践,完全替代了 Vue2 中的 Mixins 模式。