组合式函数

引言

在 Vue 3 的 Composition API 中,组合式函数(Composables) 是代码复用的核心模式。它将逻辑封装成可复用的函数,让组件更加简洁、可测试、可维护。本文将深入讲解组合式函数的设计模式、实战技巧以及常见应用场景。

什么是组合式函数?

组合式函数(Composables)是一种基于 Vue 3 Composition API 的代码组织模式。它本质上是一个以 use 开头的函数,内部调用 Vue 的响应式 API(如 refreactivecomputedwatch 等),返回响应式状态和方法供组件使用。

核心特点

  • use 开头 :遵循约定,如 useCounteruseFetch
  • 返回响应式数据 :返回 refreactive 或计算属性
  • 无组件实例依赖 :可以在任何地方调用,不依赖 this
  • 可组合、可复用:多个组合式函数可以组合使用

基础示例

1. 简单的计数器组合式函数

javascript 复制代码
<!-- useCounter.ts -->
import { ref } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  
  const increment = () => count.value++
  const decrement = () => count.value--
  const reset = () => count.value = initialValue
  
  return {
    count,
    increment,
    decrement,
    reset
  }
}
xml 复制代码
<!-- 组件中使用 -->
<template>
  <div>
    <p>计数:{{ count }}</p>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
    <button @click="reset">重置</button>
  </div>
</template>

<script setup>
import { useCounter } from './useCounter'

const { count, increment, decrement, reset } = useCounter(10)
</script>

2. 带参数的组合式函数

typescript 复制代码
<!-- useLocalStorage.ts -->
import { ref, watch } from 'vue'

export function useLocalStorage(key: string, initialValue: any) {
  // 从 localStorage 读取值
  const storedValue = localStorage.getItem(key)
  const value = ref(storedValue !== null ? JSON.parse(storedValue) : initialValue)
  
  // 监听变化并同步到 localStorage
  watch(value, (newValue) => {
    localStorage.setItem(key, JSON.stringify(newValue))
  })
  
  const setValue = (newVal: any) => {
    value.value = newVal
  }
  
  const removeValue = () => {
    localStorage.removeItem(key)
    value.value = initialValue
  }
  
  return {
    value,
    setValue,
    removeValue
  }
}

高级应用场景

1. 网络请求组合式函数

typescript 复制代码
<!-- useFetch.ts -->
import { ref, computed } from 'vue'

interface UseFetchOptions {
  immediate?: boolean
  headers?: Record<string, string>
}

export function useFetch(url: string, options: UseFetchOptions = {}) {
  const { immediate = true, headers = {} } = options
  
  const data = ref(null)
  const loading = ref(false)
  const error = ref(null)
  
  const fetched = computed(() => !loading.value && !error.value && data.value !== null)
  
  const fetchData = async () => {
    loading.value = true
    error.value = null
    
    try {
      const response = await fetch(url, { headers })
      if (!response.ok) {
        throw new Error(`HTTP 错误:${response.status}`)
      }
      data.value = await response.json()
    } catch (err: any) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }
  
  if (immediate) {
    fetchData()
  }
  
  return {
    data,
    loading,
    error,
    fetched,
    fetchData
  }
}
xml 复制代码
<!-- 组件中使用 -->
<template>
  <div>
    <div v-if="loading">加载中...</div>
    <div v-else-if="error">错误:{{ error }}</div>
    <div v-else-if="fetched">
      <h2>{{ data.title }}</h2>
      <p>{{ data.body }}</p>
    </div>
  </div>
</template>

<script setup>
import { useFetch } from './useFetch'

const { data, loading, error, fetched } = useFetch(
  'https://jsonplaceholder.typicode.com/posts/1'
)
</script>

2. 防抖和节流组合式函数

typescript 复制代码
<!-- useDebounce.ts -->
import { ref, watch, type Ref } from 'vue'

export function useDebounce<T>(source: Ref<T>, delay: number = 300) {
  const debouncedValue = ref(source.value)
  let timer: ReturnType<typeof setTimeout> | null = null
  
  watch(source, (newValue) => {
    if (timer) {
      clearTimeout(timer)
    }
    timer = setTimeout(() => {
      debouncedValue.value = newValue
    }, delay)
  })
  
  return debouncedValue
}
typescript 复制代码
<!-- useThrottle.ts -->
import { ref, watch, type Ref } from 'vue'

export function useThrottle<T>(source: Ref<T>, limit: number = 500) {
  const throttledValue = ref(source.value)
  let lastCall = 0
  let timer: ReturnType<typeof setTimeout> | null = null
  
  watch(source, (newValue) => {
    const now = Date.now()
    
    if (now - lastCall >= limit) {
      throttledValue.value = newValue
      lastCall = now
    } else {
      if (timer) clearTimeout(timer)
      timer = setTimeout(() => {
        throttledValue.value = newValue
        lastCall = Date.now()
      }, limit - (now - lastCall))
    }
  })
  
  return throttledValue
}

3. 窗口尺寸组合式函数

javascript 复制代码
<!-- useWindowSize.ts -->
import { ref, onMounted, onUnmounted } from 'vue'

export function useWindowSize() {
  const width = ref(window.innerWidth)
  const height = ref(window.innerHeight)
  
  const updateSize = () => {
    width.value = window.innerWidth
    height.value = window.innerHeight
  }
  
  onMounted(() => {
    window.addEventListener('resize', updateSize)
  })
  
  onUnmounted(() => {
    window.removeEventListener('resize', updateSize)
  })
  
  return { width, height }
}

4. 鼠标位置组合式函数

javascript 复制代码
<!-- useMousePosition.ts -->
import { ref, onMounted, onUnmounted } from 'vue'

export function useMousePosition() {
  const x = ref(0)
  const y = ref(0)
  
  const updatePosition = (event: MouseEvent) => {
    x.value = event.clientX
    y.value = event.clientY
  }
  
  onMounted(() => {
    window.addEventListener('mousemove', updatePosition)
  })
  
  onUnmounted(() => {
    window.removeEventListener('mousemove', updatePosition)
  })
  
  return { x, y }
}

组合多个组合式函数

组合式函数的强大之处在于可以相互组合:

javascript 复制代码
<!-- useSearch.ts -->
import { ref, computed } from 'vue'
import { useDebounce } from './useDebounce'
import { useFetch } from './useFetch'

export function useSearch(apiUrl: string) {
  const searchTerm = ref('')
  const debouncedSearch = useDebounce(searchTerm, 300)
  
  const { 
    data: results, 
    loading, 
    error 
  } = useFetch(
    computed(() => `${apiUrl}?q=${debouncedSearch.value}`),
    { immediate: false }
  )
  
  const search = () => {
    // 触发搜索
  }
  
  const hasResults = computed(() => results.value && results.value.length > 0)
  
  return {
    searchTerm,
    results,
    loading,
    error,
    hasResults,
    search
  }
}

最佳实践

1. 命名规范

  • use 开头,如 useCounteruseFetch
  • 函数名清晰表达功能
  • 返回对象使用有意义的属性名

2. 类型安全

使用 TypeScript 提供完整的类型支持:

typescript 复制代码
interface UseCounterReturn {
  count: Ref<number>
  increment: () => void
  decrement: () => void
  reset: () => void
}

export function useCounter(initialValue: number = 0): UseCounterReturn {
  // ...
}

3. 解构保持响应性

使用 toRefs 确保解构后保持响应性:

php 复制代码
<!-- useStore.ts -->
import { reactive, toRefs } from 'vue'

export function useStore() {
  const state = reactive({
    user: null,
    token: null,
    loading: false
  })
  
  return toRefs(state)
}

4. 错误处理

在组合式函数内部处理错误,返回错误状态:

typescript 复制代码
export function useAsync(fn: () => Promise<any>) {
  const loading = ref(false)
  const error = ref(null)
  const data = ref(null)
  
  const execute = async () => {
    loading.value = true
    error.value = null
    
    try {
      data.value = await fn()
    } catch (err: any) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }
  
  return { loading, error, data, execute }
}

总结

组合式函数是 Vue 3 中最强大的代码复用模式。通过:

  • 逻辑封装:将相关功能打包成独立函数
  • 响应式集成:无缝使用 Vue 的响应式系统
  • 可组合性:多个组合式函数可以相互组合
  • 类型安全:完整的 TypeScript 支持

掌握组合式函数,能让你的 Vue 代码更加简洁、可维护、可测试。建议从简单的功能开始实践,逐步构建自己的组合式函数库。

相关推荐
CodeSheep1 小时前
中国编程第一人,一人抵一城!
前端·后端·程序员
GISer_Jing1 小时前
Claude Code项目配置终极指南
前端·ai·ai编程
MPGWJPMTJT1 小时前
从 Volta 迁移到 mise:Windows 下 Node 版本管理切换记录
前端·node.js
米丘1 小时前
vue3.x 调度器(Scheduler)实现机制
前端·javascript·vue.js
米饭同学i1 小时前
Vue2 + Webpack 老项目启动报错
前端
小彭努力中1 小时前
205.Vue3 + OpenLayers:加载动画,采用 CSS 的 @keyframes 方式
前端·css·vue.js·openlayers·cesium·webgis
木斯佳1 小时前
前端八股文面经大全:上海威派格前端实习(2026-05-07)·面经深度解析
前端
_Twink1e1 小时前
基于Vue的纯前端的库存销售系统
前端·vue.js·vue·web
幽络源小助理1 小时前
音频在线剪切助手网页版源码 – 纯前端HTML单文件免费分享
前端·音视频