组合式函数

引言

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

什么是组合式函数?

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

核心特点

  • 以 use 开头 :遵循约定,如 useCounter、useFetch
  • 返回响应式数据 :返回 ref、reactive 或计算属性
  • 无组件实例依赖 :可以在任何地方调用,不依赖 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 开头,如 useCounter、useFetch
  • 函数名清晰表达功能
  • 返回对象使用有意义的属性名

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 代码更加简洁、可维护、可测试。建议从简单的功能开始实践,逐步构建自己的组合式函数库。

相关推荐
康小汪2 小时前
AI 写 Vue 总写成同一个味道:给它装 Skills,装的不是知识是边界
前端·vue.js·人工智能·ai编程·最佳实践
智能运维指南3 小时前
研制过程“两张皮“怎么破:研制一体化支撑平台的定位、能力边界与落地前提
java·前端·人工智能·嘉为蓝鲸
白山编程大哥4 小时前
Java 后端如何优雅地把 List 集合返回给前端
java·前端·list
Flynt5 小时前
我把热榜那篇"禁用 axios"逐条实测了一遍:结论大体对,坑在它没讲的地方
前端·javascript·axios
Csvn6 小时前
bundle优化
前端
北冥有鱼6 小时前
发版后动态路由点击报 `Failed to fetch dynamically imported module`」问题上的完整排查与修复过程
前端·vue.js
静默回滚6 小时前
视频压到 25 MB 以内,码率要算、音轨要扣、余量要留
前端
用户37203749260186 小时前
把 10MB 中文语义模型塞进浏览器:STSB 做到教师模型的 93%,但线上 top1 只有 42%
前端
光锥智能6 小时前
海康机器人举行2026移动机器人新品发布会:箱式存拣升级,「明智」系列发布
java·前端·机器人
计算机魔术师6 小时前
Anthropic 上市文件最吓人的一幕,不是亏损 807 亿
前端