Skip to content

Vue 3 Composition API 最佳实践

Vue 3 的 Composition API 为我们提供了更灵活、更强大的组件逻辑组织方式。本文将深入探讨其核心概念和最佳实践。

为什么选择 Composition API

相比于 Options API,Composition API 具有以下优势:

  • 更好的逻辑复用:通过组合函数实现逻辑复用
  • 更好的类型推导:TypeScript 支持更加完善
  • 更灵活的代码组织:相关逻辑可以组织在一起
  • 更小的打包体积:支持 tree-shaking

核心概念

ref vs reactive

typescript
import { ref, reactive } from 'vue'

// ref:用于基本类型
const count = ref(0)
const message = ref('Hello')

// reactive:用于对象类型
const state = reactive({
  user: {
    name: 'John',
    age: 30
  },
  posts: []
})

computed 计算属性

typescript
import { computed, ref } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

// 只读计算属性
const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`
})

// 可写计算属性
const fullNameWritable = computed({
  get() {
    return `${firstName.value} ${lastName.value}`
  },
  set(value) {
    [firstName.value, lastName.value] = value.split(' ')
  }
})

watch 和 watchEffect

typescript
import { watch, watchEffect, ref } from 'vue'

const count = ref(0)
const message = ref('hello')

// watch:明确指定依赖
watch(count, (newValue, oldValue) => {
  console.log(`count changed from ${oldValue} to ${newValue}`)
})

// 监听多个源
watch([count, message], ([newCount, newMessage], [oldCount, oldMessage]) => {
  console.log('Multiple values changed')
})

// watchEffect:自动收集依赖
watchEffect(() => {
  console.log(`Count is ${count.value}, message is ${message.value}`)
})

最佳实践

1. 组合函数设计原则

单一职责原则

typescript
// ✅ 好的做法:单一职责
function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  
  const increment = () => count.value++
  const decrement = () => count.value--
  const reset = () => count.value = initialValue
  
  return {
    count: readonly(count),
    increment,
    decrement,
    reset
  }
}

// ❌ 避免:职责混乱
function useCounterAndUser() {
  // 计数器逻辑
  const count = ref(0)
  // 用户逻辑
  const user = ref(null)
  // ...
}

一致的返回值

typescript
// ✅ 好的做法:一致的返回模式
function useApi<T>(url: string) {
  const data = ref<T | null>(null)
  const loading = ref(false)
  const error = ref<string | null>(null)
  
  const execute = async () => {
    loading.value = true
    error.value = null
    
    try {
      const response = await fetch(url)
      data.value = await response.json()
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }
  
  return {
    data: readonly(data),
    loading: readonly(loading),
    error: readonly(error),
    execute
  }
}

2. 生命周期钩子

typescript
import { onMounted, onUnmounted, onUpdated } from 'vue'

function useEventListener(target: EventTarget, event: string, handler: Function) {
  onMounted(() => {
    target.addEventListener(event, handler)
  })
  
  onUnmounted(() => {
    target.removeEventListener(event, handler)
  })
}

// 使用
function useWindowResize() {
  const width = ref(window.innerWidth)
  const height = ref(window.innerHeight)
  
  const updateSize = () => {
    width.value = window.innerWidth
    height.value = window.innerHeight
  }
  
  useEventListener(window, 'resize', updateSize)
  
  return { width, height }
}

3. 错误处理

typescript
function useAsyncOperation<T>() {
  const data = ref<T | null>(null)
  const loading = ref(false)
  const error = ref<Error | null>(null)
  
  const execute = async (operation: () => Promise<T>) => {
    try {
      loading.value = true
      error.value = null
      data.value = await operation()
    } catch (err) {
      error.value = err instanceof Error ? err : new Error(String(err))
      console.error('Async operation failed:', err)
    } finally {
      loading.value = false
    }
  }
  
  const reset = () => {
    data.value = null
    error.value = null
    loading.value = false
  }
  
  return {
    data: readonly(data),
    loading: readonly(loading),
    error: readonly(error),
    execute,
    reset
  }
}

性能优化

1. 使用 shallowRef 和 shallowReactive

typescript
import { shallowRef, shallowReactive } from 'vue'

// 对于大型对象,使用 shallow 版本
const largeList = shallowRef([])
const config = shallowReactive({
  theme: 'dark',
  language: 'zh-CN'
})

2. 使用 markRaw

typescript
import { markRaw, reactive } from 'vue'

const state = reactive({
  // 第三方库实例不需要响应式
  chart: markRaw(new Chart()),
  data: []
})

3. 合理使用 computed

typescript
// ✅ 好的做法:缓存复杂计算
const expensiveValue = computed(() => {
  return heavyCalculation(props.data)
})

// ❌ 避免:在模板中直接调用函数
// <template>
//   <div>{{ heavyCalculation(data) }}</div>
// </template>

实际应用示例

表单处理

typescript
function useForm<T extends Record<string, any>>(initialValues: T) {
  const values = reactive({ ...initialValues })
  const errors = reactive<Partial<Record<keyof T, string>>>({})
  const touched = reactive<Partial<Record<keyof T, boolean>>>({})
  
  const setFieldValue = (field: keyof T, value: any) => {
    values[field] = value
    touched[field] = true
    // 清除错误
    if (errors[field]) {
      delete errors[field]
    }
  }
  
  const setFieldError = (field: keyof T, error: string) => {
    errors[field] = error
  }
  
  const validate = (rules: Partial<Record<keyof T, (value: any) => string | undefined>>) => {
    let isValid = true
    
    Object.keys(rules).forEach(field => {
      const rule = rules[field as keyof T]
      if (rule) {
        const error = rule(values[field as keyof T])
        if (error) {
          setFieldError(field as keyof T, error)
          isValid = false
        }
      }
    })
    
    return isValid
  }
  
  const reset = () => {
    Object.assign(values, initialValues)
    Object.keys(errors).forEach(key => delete errors[key])
    Object.keys(touched).forEach(key => delete touched[key])
  }
  
  return {
    values,
    errors: readonly(errors),
    touched: readonly(touched),
    setFieldValue,
    setFieldError,
    validate,
    reset
  }
}

数据获取

typescript
function useFetch<T>(url: string, options?: RequestInit) {
  const data = ref<T | null>(null)
  const loading = ref(false)
  const error = ref<string | null>(null)
  
  const execute = async () => {
    loading.value = true
    error.value = null
    
    try {
      const response = await fetch(url, options)
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`)
      }
      data.value = await response.json()
    } catch (err) {
      error.value = err instanceof Error ? err.message : 'Unknown error'
    } finally {
      loading.value = false
    }
  }
  
  // 自动执行
  onMounted(execute)
  
  return {
    data: readonly(data),
    loading: readonly(loading),
    error: readonly(error),
    refetch: execute
  }
}

总结

Composition API 为 Vue 3 带来了更强大的逻辑组织能力。通过遵循最佳实践,我们可以:

  1. 编写更可维护的代码
  2. 实现更好的逻辑复用
  3. 获得更好的 TypeScript 支持
  4. 优化应用性能

记住,好的代码不仅要功能正确,还要易于理解和维护。Composition API 为我们提供了实现这一目标的工具,但如何使用这些工具仍然需要我们的智慧和经验。

用代码构建未来,让技术改变世界 🚀 | 专注前端技术,分享开发经验