1. 为什么需要自定义组合式函数在Vue3项目开发中我们经常会遇到需要复用逻辑的场景。传统Vue2中使用Mixins来实现逻辑复用但这种方式存在几个明显问题命名冲突风险多个Mixins可能定义相同名称的属性或方法来源不清晰很难快速判断某个属性来自哪个Mixin关系复杂Mixins之间可能形成隐式依赖关系类型支持弱TypeScript类型推断不够友好组合式APIComposition API正是为了解决这些问题而设计的。通过自定义组合式函数我们可以将相关逻辑组织在一起明确暴露需要使用的值和方法保持响应式特性获得更好的TypeScript支持2. 基础组合式函数示例useMouse让我们从一个简单的鼠标位置跟踪器开始看看如何创建自定义组合式函数// useMouse.ts import { ref, onMounted, onUnmounted } from vue export function useMouse() { const x ref(0) const y ref(0) function update(event: MouseEvent) { x.value event.pageX y.value event.pageY } onMounted(() window.addEventListener(mousemove, update)) onUnmounted(() window.removeEventListener(mousemove, update)) return { x, y } }在组件中使用script setup import { useMouse } from ./useMouse const { x, y } useMouse() /script template div鼠标位置{{ x }}, {{ y }}/div /template这个简单的例子展示了组合式函数的核心特点使用ref创建响应式数据在setup阶段执行副作用事件监听明确返回需要暴露的值自动清理副作用3. 组合式函数与Mixins的对比让我们通过一个更复杂的例子来对比两种方式的差异。假设我们需要实现一个数据获取逻辑3.1 Mixins实现方式// fetchMixin.js export default { data() { return { loading: false, data: null, error: null } }, methods: { async fetchData(url) { this.loading true try { const response await fetch(url) this.data await response.json() } catch (err) { this.error err } finally { this.loading false } } } }使用Mixins的问题无法知道loading/data/error来自哪个Mixin如果多个Mixins都有这些属性会冲突方法命名可能冲突类型支持有限3.2 组合式函数实现// useFetch.ts import { ref } from vue export function useFetchT(url: string) { const data refT | null(null) const error ref(null) const loading ref(false) async function doFetch() { loading.value true try { const response await fetch(url) data.value await response.json() } catch (err) { error.value err } finally { loading.value false } } doFetch() return { data, error, loading, retry: doFetch } }使用方式script setup langts import { useFetch } from ./useFetch interface User { id: number name: string } const { data: users, loading, error } useFetchUser[](/api/users) /script组合式函数的优势明确的数据来源可重命名返回值避免冲突完整的TypeScript支持清晰的依赖关系4. 高级组合式函数模式4.1 接受响应式参数组合式函数可以接受ref作为参数自动响应变化export function useFetchT(url: Refstring) { const data refT | null(null) // ... watch(url, (newUrl) { doFetch(newUrl) }, { immediate: true }) }4.2 可配置选项export function useFetchT(url: string, options?: { immediate?: boolean timeout?: number }) { // 实现... }4.3 组合其他组合式函数export function useMouseTracker() { const { x, y } useMouse() const distance computed(() Math.sqrt(x.value ** 2 y.value ** 2)) return { x, y, distance } }5. 最佳实践与常见问题5.1 命名约定始终以use开头使用camelCase命名例如useMouse, useFetch, useDarkMode5.2 副作用管理在onMounted/onUnmounted中管理生命周期使用watchEffect自动清理返回清理函数export function useInterval(callback: () void, delay: number) { let intervalId: number onMounted(() { intervalId setInterval(callback, delay) }) onUnmounted(() { clearInterval(intervalId) }) // 也可以返回停止函数 function stop() { clearInterval(intervalId) } return { stop } }5.3 类型安全为输入输出提供明确的TypeScript类型使用泛型支持多种数据类型为可选参数提供默认值export function useLocalStorageT(key: string, initialValue: T) { const stored localStorage.getItem(key) const data refT(stored ? JSON.parse(stored) : initialValue) watch(data, (newValue) { localStorage.setItem(key, JSON.stringify(newValue)) }, { deep: true }) return data }5.4 性能优化使用computed减少不必要的计算使用shallowRef处理大型对象避免在渲染函数中创建新的对象export function useLargeData() { const bigData shallowRef({ /* 大型对象 */ }) // 只在使用时解构 return { bigData } }6. 实际应用案例6.1 表单验证export function useFormValidation(form: RefRecordstring, any, rules: ValidationRules) { const errors reactiveRecordstring, string({}) function validate() { let isValid true for (const [field, rule] of Object.entries(rules)) { const error rule(form.value[field]) if (error) { errors[field] error isValid false } else { delete errors[field] } } return isValid } return { errors, validate } }6.2 主题切换export function useTheme() { const theme reflight | dark( localStorage.getItem(theme) as light | dark || light ) watch(theme, (newTheme) { document.documentElement.setAttribute(data-theme, newTheme) localStorage.setItem(theme, newTheme) }, { immediate: true }) function toggle() { theme.value theme.value light ? dark : light } return { theme, toggle } }6.3 无限滚动export function useInfiniteScroll(fetchMore: () Promisevoid) { const loading ref(false) const observer refIntersectionObserver | null(null) const sentinel refHTMLElement | null(null) onMounted(() { observer.value new IntersectionObserver(async (entries) { if (entries[0].isIntersecting !loading.value) { loading.value true await fetchMore() loading.value false } }) if (sentinel.value) { observer.value.observe(sentinel.value) } }) onUnmounted(() { observer.value?.disconnect() }) return { loading, sentinel } }7. 从Mixins迁移到组合式函数如果你有现有的Mixins可以按照以下步骤迁移识别Mixin中的数据和逻辑将相关逻辑提取到组合式函数在组件中导入并使用组合式函数删除Mixin引用测试功能是否正常例如将之前的fetchMixin迁移// 之前 export default { mixins: [fetchMixin], created() { this.fetchData(/api/users) } } // 之后 import { useFetch } from ./useFetch export default { setup() { const { data: users, loading, error } useFetch(/api/users) return { users, loading, error } } }8. 组合式函数的测试组合式函数可以像普通函数一样测试import { useCounter } from ./useCounter import { ref } from vue test(useCounter, () { const { count, increment } useCounter(ref(0)) expect(count.value).toBe(0) increment() expect(count.value).toBe(1) increment(5) expect(count.value).toBe(6) })对于涉及DOM或生命周期的函数可以使用vue/test-utilsimport { mount } from vue/test-utils import { useMouse } from ./useMouse test(useMouse, async () { const wrapper mount({ template: div/div, setup() { return useMouse() } }) window.dispatchEvent(new MouseEvent(mousemove, { clientX: 100, clientY: 200 })) await nextTick() expect(wrapper.vm.x).toBe(100) expect(wrapper.vm.y).toBe(200) })9. 组合式函数的共享与复用9.1 创建可重用库将常用的组合式函数组织成独立包/src /composables useMouse.ts useFetch.ts useLocalStorage.ts index.ts // 统一导出9.2 社区资源许多优秀的Vue3组合式函数库已经存在VueUsehttps://vueuse.org/vue/composition-apiVue2支持各种UI库提供的组合式函数9.3 在团队中共享创建内部工具库编写文档和示例进行代码审查确保质量10. 常见问题解答10.1 什么时候应该使用组合式函数当你有需要复用的逻辑时特别是涉及多个响应式数据包含副作用需要生命周期管理在多个组件中重复使用10.2 组合式函数可以替代组件吗不完全是。组合式函数专注于逻辑复用而组件关注UI和逻辑的组合。通常你会用组合式函数封装逻辑用组件封装UI和状态在组件中使用组合式函数10.3 组合式函数会导致性能问题吗合理使用不会。注意避免在渲染函数中创建大量响应式对象使用shallowRef/shallowReactive处理大型数据合理使用computed和watch10.4 如何调试组合式函数使用Vue DevTools查看组合式函数状态添加console.log调试使用debugger语句编写单元测试10.5 组合式函数能访问组件实例吗不应该直接访问。组合式函数应该通过参数接收需要的数据通过返回值提供功能保持与组件的解耦如果需要访问组件上下文可以使用getCurrentInstance但不推荐。11. 总结与进阶建议组合式函数是Vue3中最强大的特性之一它解决了Mixins的诸多问题提供了更好的代码组织和复用方式。在实际项目中从小型组合式函数开始逐步积累遵循单一职责原则保持函数专注编写清晰的类型定义和文档为复杂逻辑编写单元测试与团队成员共享最佳实践进阶学习方向探索VueUse源码学习如何组合多个组合式函数研究如何优化性能尝试创建自己的组合式函数库