Vue 3 Composition API 深度解析

AI Content Team
22 分钟阅读

全面讲解 Vue 3 Composition API 的用法、最佳实践和高级模式

Vue 3 Composition API 深度解析

Composition API 让 Vue 应用更易于组织和重用逻辑。本文深入讲解这一核心特性。

核心概念

setup 函数

import { ref, computed, watch } from 'vue'

export default {
  props: ['initialCount'],
  emits: ['count-changed'],
  
  setup(props, { emit, slots, expose }) {
    // 创建响应式状态
    const count = ref(props.initialCount)
    const doubled = computed(() => count.value * 2)
    
    // 监听状态变化
    watch(count, (newVal, oldVal) => {
      console.log(`Count changed from ${oldVal} to ${newVal}`)
      emit('count-changed', newVal)
    })
    
    // 定义方法
    const increment = () => count.value++
    const decrement = () => count.value--
    
    // 返回模板需要的内容
    return {
      count,
      doubled,
      increment,
      decrement,
    }
  },
}

响应式基础

import { ref, reactive, readonly, isRef } from 'vue'

// ref - 用于基本类型
const count = ref(0)
console.log(count.value) // 0
count.value++

// reactive - 用于对象
const state = reactive({
  name: 'John',
  age: 30,
  address: {
    city: 'Beijing',
  },
})

state.name = 'Jane' // 自动更新,无需 .value

// readonly - 创建只读副本
const original = reactive({ count: 0 })
const copy = readonly(original)
// copy.count++ // 错误:不能修改

// isRef 检查
console.log(isRef(count)) // true
console.log(isRef(state)) // false

组合式函数

创建可重用逻辑

// useCounter.js - 组合式函数
import { ref, computed } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  const doubled = computed(() => count.value * 2)
  
  const increment = () => count.value++
  const decrement = () => count.value--
  const reset = () => count.value = initialValue
  
  return {
    count,
    doubled,
    increment,
    decrement,
    reset,
  }
}

// useFetch.js - 数据获取组合式函数
import { ref, onMounted } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const loading = ref(false)
  const error = ref(null)
  
  const fetch = async () => {
    loading.value = true
    error.value = null
    
    try {
      const response = await fetch(url)
      data.value = await response.json()
    } catch (e) {
      error.value = e
    } finally {
      loading.value = false
    }
  }
  
  onMounted(fetch)
  
  return {
    data,
    loading,
    error,
    refetch: fetch,
  }
}

// 使用
export default {
  setup() {
    const { count, doubled, increment } = useCounter(10)
    const { data, loading, refetch } = useFetch('/api/data')
    
    return {
      count,
      doubled,
      increment,
      data,
      loading,
      refetch,
    }
  },
}

生命周期钩子

Composition API 中的生命周期

import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
  onErrorCaptured,
} from 'vue'

export default {
  setup() {
    onBeforeMount(() => {
      console.log('组件挂载前')
    })
    
    onMounted(() => {
      console.log('组件已挂载')
      // 初始化事件监听器、定时器等
    })
    
    onBeforeUpdate(() => {
      console.log('组件更新前')
    })
    
    onUpdated(() => {
      console.log('组件已更新')
    })
    
    onBeforeUnmount(() => {
      console.log('组件卸载前')
    })
    
    onUnmounted(() => {
      console.log('组件已卸载')
      // 清理事件监听器、定时器等
    })
    
    onErrorCaptured((err, instance, info) => {
      console.log('捕获错误:', err)
      return false // 返回 false 阻止错误传播
    })
    
    return {}
  },
}

模板引用

访问 DOM 元素

import { ref, onMounted } from 'vue'

export default {
  setup() {
    const inputRef = ref(null)
    const listRef = ref(null)
    const dynamicRef = ref(null)
    
    onMounted(() => {
      // 访问 DOM 元素
      inputRef.value?.focus()
      console.log(listRef.value?.offsetHeight)
    })
    
    // 函数式引用
    const assignRef = el => {
      if (el) {
        console.log('元素已赋值', el)
      } else {
        console.log('元素已移除')
      }
    }
    
    return {
      inputRef,
      listRef,
      dynamicRef,
      assignRef,
    }
  },
  
  template: \`
    <div>
      <input ref="inputRef" />
      <ul ref="listRef">
        <li v-for="item in items" :key="item">{{ item }}</li>
      </ul>
      <div :ref="assignRef"></div>
    </div>
  \`,
}

依赖注入

跨组件共享数据

import { provide, inject, ref, readonly } from 'vue'

// 父组件
export default {
  setup() {
    const theme = ref('light')
    const user = ref({ name: 'John', role: 'admin' })
    
    // 提供数据给子组件
    provide('theme', readonly(theme))
    provide('updateTheme', (newTheme) => {
      theme.value = newTheme
    })
    
    // 使用 Symbol 作为 key 避免命名冲突
    const userKey = Symbol()
    provide(userKey, readonly(user))
    
    return {
      theme,
      updateTheme: (newTheme) => {
        theme.value = newTheme
      },
    }
  },
}

// 子组件
export default {
  setup() {
    // 注入数据
    const theme = inject('theme')
    const updateTheme = inject('updateTheme')
    const user = inject(Symbol.for('user'))
    
    // 带默认值的注入
    const config = inject('config', {
      apiUrl: 'http://localhost:3000',
    })
    
    return {
      theme,
      updateTheme,
      user,
      config,
    }
  },
}

高级状态管理

创建小型 store

import { reactive, readonly, computed } from 'vue'

// store.js - 不依赖 Pinia 的简单 store
export function createStore() {
  const state = reactive({
    items: [],
    filter: 'all',
    sortBy: 'date',
  })
  
  const filteredItems = computed(() => {
    let result = state.items
    
    if (state.filter !== 'all') {
      result = result.filter(item => item.status === state.filter)
    }
    
    if (state.sortBy === 'date') {
      result.sort((a, b) => new Date(b.date) - new Date(a.date))
    } else if (state.sortBy === 'name') {
      result.sort((a, b) => a.name.localeCompare(b.name))
    }
    
    return result
  })
  
  const actions = {
    addItem(item) {
      state.items.push({ ...item, id: Date.now() })
    },
    
    removeItem(id) {
      state.items = state.items.filter(item => item.id !== id)
    },
    
    updateItem(id, updates) {
      const item = state.items.find(item => item.id === id)
      if (item) {
        Object.assign(item, updates)
      }
    },
    
    setFilter(filter) {
      state.filter = filter
    },
    
    setSortBy(sortBy) {
      state.sortBy = sortBy
    },
  }
  
  return {
    state: readonly(state),
    filteredItems,
    ...actions,
  }
}

// 使用
export default {
  setup() {
    const store = createStore()
    
    const handleAdd = (item) => {
      store.addItem(item)
    }
    
    return {
      items: store.filteredItems,
      addItem: handleAdd,
      setFilter: store.setFilter,
    }
  },
}

最佳实践

应该做的事:

  • 将相关逻辑组织在一起
  • 创建可重用的组合式函数
  • 使用 TypeScript 获得更好的类型检查
  • 合理使用计算属性和监听器
  • 及时清理事件监听器和定时器

不应该做的事:

  • 在 setup 中执行副作用操作(除了生命周期钩子)
  • 过度使用计算属性
  • 忘记清理 watch 监听器
  • 在 reactive 对象中存储引用类型时不谨慎
  • 过度复杂化组合式函数

检查清单

  • 正确使用 ref 和 reactive
  • 生命周期钩子正确
  • 模板引用工作正常
  • 组合式函数可重用
  • 性能优化得当