第6章:组合式API

第17节:组合式函数(Composables)

6.17.1 自定义组合式函数创建

组合式函数(Composables)是Vue 3中用于复用逻辑的重要特性,它允许我们将组件中的响应式逻辑提取到可复用的函数中。

基本概念

组合式函数是一个函数,它:

  1. 接收响应式数据作为输入
  2. 创建和管理自己的响应式状态
  3. 返回响应式数据和方法
  4. 可以使用其他组合式函数

示例:鼠标位置跟踪

// 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 }
}

在组件中使用:

<template>
  <div>
    <h3>鼠标位置跟踪</h3>
    <p>X: {{ x }}</p>
    <p>Y: {{ y }}</p>
  </div>
</template>

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

// 使用组合式函数
const { x, y } = useMouse()
</script>

示例:计数器逻辑

// useCounter.js
import { ref, computed } from 'vue'

export function useCounter(initialValue = 0) {
  // 定义响应式状态
  const count = ref(initialValue)
  
  // 定义计算属性
  const doubleCount = computed(() => count.value * 2)
  
  // 定义方法
  const increment = () => count.value++
  const decrement = () => count.value--
  const reset = () => count.value = initialValue
  
  // 返回响应式状态和方法
  return {
    count,
    doubleCount,
    increment,
    decrement,
    reset
  }
}

在组件中使用:

<template>
  <div>
    <h3>计数器</h3>
    <p>count: {{ count }}</p>
    <p>doubleCount: {{ doubleCount }}</p>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
    <button @click="reset">重置</button>
  </div>
</template>

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

// 使用组合式函数,初始值为10
const { count, doubleCount, increment, decrement, reset } = useCounter(10)
</script>

6.17.2 常用组合式函数示例

组合式函数可以用于各种场景,下面是一些常用的组合式函数示例:

useFetch:数据获取

// useFetch.js
import { ref, onMounted, watch } from 'vue'

export function useFetch(url) {
  // 定义响应式状态
  const data = ref(null)
  const error = ref(null)
  const loading = ref(false)
  
  // 定义数据获取函数
  const fetchData = async () => {
    loading.value = true
    error.value = null
    
    try {
      const response = await fetch(url)
      if (!response.ok) {
        throw new Error('请求失败')
      }
      data.value = await response.json()
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }
  
  // 监听url变化,重新获取数据
  watch(url, (newUrl) => {
    if (newUrl) {
      fetchData()
    }
  })
  
  // 组件挂载时获取数据
  onMounted(() => {
    if (url.value) {
      fetchData()
    }
  })
  
  // 返回响应式状态和方法
  return {
    data,
    error,
    loading,
    refetch: fetchData
  }
}

在组件中使用:

<template>
  <div>
    <h3>数据获取示例</h3>
    <input v-model="url" placeholder="输入API地址" />
    <button @click="refetch">重新获取</button>
    
    <div v-if="loading">加载中...</div>
    <div v-else-if="error">错误: {{ error }}</div>
    <div v-else-if="data">
      <pre>{{ JSON.stringify(data, null, 2) }}</pre>
    </div>
  </div>
</template>

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

const url = ref('https://jsonplaceholder.typicode.com/todos/1')
const { data, error, loading, refetch } = useFetch(url)
</script>

useLocalStorage:本地存储

// useLocalStorage.js
import { ref, watch } from 'vue'

export function useLocalStorage(key, initialValue) {
  // 从本地存储获取初始值
  const storedValue = localStorage.getItem(key)
  const value = ref(storedValue ? JSON.parse(storedValue) : initialValue)
  
  // 监听value变化,同步到本地存储
  watch(value, (newValue) => {
    localStorage.setItem(key, JSON.stringify(newValue))
  }, { deep: true })
  
  // 返回响应式状态
  return value
}

在组件中使用:

<template>
  <div>
    <h3>本地存储示例</h3>
    <input v-model="name" placeholder="输入姓名" />
    <p>姓名: {{ name }}</p>
  </div>
</template>

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

// 使用本地存储,键名为'userName',初始值为空字符串
const name = useLocalStorage('userName', '')
</script>

useDebounce:防抖

// useDebounce.js
import { ref, watch } from 'vue'

export function useDebounce(value, delay = 300) {
  const debouncedValue = ref(value.value)
  let timer = null
  
  watch(value, (newValue) => {
    // 清除之前的定时器
    if (timer) {
      clearTimeout(timer)
    }
    
    // 设置新的定时器
    timer = setTimeout(() => {
      debouncedValue.value = newValue
    }, delay)
  })
  
  return debouncedValue
}

在组件中使用:

<template>
  <div>
    <h3>防抖示例</h3>
    <input v-model="inputValue" placeholder="输入搜索内容" />
    <p>防抖后的值: {{ debouncedValue }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useDebounce } from './useDebounce'

const inputValue = ref('')
// 使用防抖,延迟500ms
const debouncedValue = useDebounce(inputValue, 500)
</script>

useThrottle:节流

// useThrottle.js
import { ref, watch } from 'vue'

export function useThrottle(value, delay = 300) {
  const throttledValue = ref(value.value)
  let lastUpdateTime = 0
  
  watch(value, (newValue) => {
    const now = Date.now()
    if (now - lastUpdateTime >= delay) {
      lastUpdateTime = now
      throttledValue.value = newValue
    }
  })
  
  return throttledValue
}

6.17.3 组合式函数的最佳实践

命名约定

  • 使用use前缀命名组合式函数,如useMouseuseFetch
  • 函数名应清晰反映其功能
  • 内部变量名应遵循驼峰命名法

响应式状态管理

  • 使用refreactive创建响应式状态
  • 避免在组合式函数中直接修改外部状态
  • 对于复杂状态,考虑使用reactive进行组织

生命周期管理

  • 在组合式函数内部管理自己的生命周期
  • 使用onMountedonUnmounted等钩子清理资源
  • 避免在组合式函数中依赖外部组件的生命周期

依赖注入

  • 对于需要全局状态的组合式函数,可以使用provideinject
  • 考虑使用readonly包装注入的数据,防止意外修改

类型安全

  • 在TypeScript中,为组合式函数添加类型定义
  • 使用泛型支持不同类型的数据
  • 为返回值添加明确的类型

示例:带类型的useFetch

// useFetch.ts
import { ref, onMounted, watch, Ref } from 'vue'

export function useFetch<T>(url: Ref<string | null>) {
  const data = ref<T | null>(null)
  const error = ref<string | null>(null)
  const loading = ref(false)
  
  const fetchData = async () => {
    if (!url.value) return
    
    loading.value = true
    error.value = null
    
    try {
      const response = await fetch(url.value)
      if (!response.ok) {
        throw new Error('请求失败')
      }
      data.value = await response.json()
    } catch (err) {
      error.value = err instanceof Error ? err.message : '未知错误'
    } finally {
      loading.value = false
    }
  }
  
  watch(url, (newUrl) => {
    if (newUrl) {
      fetchData()
    }
  })
  
  onMounted(() => {
    if (url.value) {
      fetchData()
    }
  })
  
  return {
    data,
    error,
    loading,
    refetch: fetchData
  }
}

6.17.4 组合式函数与Mixin对比

Mixin的问题

  1. 命名冲突:不同Mixin可能定义相同名称的变量或方法,导致冲突
  2. 来源不明确:组件中使用的变量和方法可能来自多个Mixin,难以追踪来源
  3. 依赖关系不清晰:Mixin之间的依赖关系不明确,容易产生副作用
  4. 代码复用不灵活:Mixin是静态组合,无法根据条件动态使用

组合式函数的优势

  1. 明确的来源:通过解构赋值,变量和方法的来源清晰
  2. 无命名冲突:可以重命名变量和方法,避免冲突
  3. 灵活的组合:可以根据条件动态使用,支持嵌套组合
  4. 类型安全:在TypeScript中具有更好的类型支持
  5. 更好的可维护性:代码逻辑封装在函数内部,便于测试和维护

对比示例

使用Mixin

// counterMixin.js
export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    }
  }
}

// 使用Mixin
new Vue({
  mixins: [counterMixin],
  mounted() {
    this.increment() // 来源不明确
    console.log(this.count) // 来源不明确
  }
})

使用组合式函数

// useCounter.js
export function useCounter() {
  const count = ref(0)
  const increment = () => count.value++
  return { count, increment }
}

// 使用组合式函数
const { count, increment } = useCounter() // 来源明确
increment()
console.log(count.value)

总结

组合式函数是Vue 3中用于复用逻辑的重要特性,它具有以下优点:

  1. 更好的代码组织:将相关逻辑封装在一个函数中,便于维护和测试
  2. 明确的依赖关系:通过参数和返回值,明确函数之间的依赖关系
  3. 灵活的组合:可以根据需要组合多个组合式函数
  4. 类型安全:在TypeScript中具有更好的类型支持
  5. 无命名冲突:通过解构赋值,避免命名冲突
  6. 更好的性能:只包含必要的逻辑,没有额外的开销

通过学习和使用组合式函数,你可以编写出更加模块化、可复用和可维护的Vue代码。组合式函数是Vue 3中推荐的逻辑复用方式,它解决了Mixin存在的诸多问题,提供了更好的开发体验。

在下一章中,我们将学习Vue Router,这是Vue官方的路由管理库,用于实现单页面应用的路由功能。

« 上一篇 生命周期钩子与依赖注入 下一篇 » Vue Router基础