Vue3 Composition API 深度实战指南:从入门到精通

Vue3 引入了全新的 Composition API,这是一种组织组件逻辑的新方式,相比于传统的 Options API,它在代码组织、逻辑复用和类型推断方面有着显著优势。本文将带你深入了解 Composition API 的核心概念和实战技巧。

为什么需要 Composition API?

在 Vue2 的 Options API 中,相关逻辑分散在不同的选项中(data、methods、computed、watch 等)。当组件变得复杂时,相关逻辑被拆分到各处,导致代码难以维护和复用。Composition API 的出现解决了这些问题:

  • 更好的逻辑组织:相关逻辑可以放在一起,而不是分散在各个选项中
  • 更强的逻辑复用:Composables(组合式函数)让逻辑复用变得简单
  • 更好的类型推断:与 TypeScript 结合更紧密,类型推断更准确
  • 更小的打包体积:更好的 tree-shaking 支持

核心概念:setup() 函数

Composition API 的核心是 setup() 函数,它是组件中使用 Composition API 的入口点。

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

export default {
  setup(props, context) {
    // 响应式数据
    const count = ref(0)
    const message = ref('Hello Vue3')
    
    // 计算属性
    const doubledCount = computed(() => count.value * 2)
    
    // 方法
    function increment() {
      count.value++
    }
    
    // 生命周期钩子
    onMounted(() => {
      console.log('组件已挂载')
    })
    
    // 暴露给模板
    return {
      count,
      message,
      doubledCount,
      increment
    }
  }
}

响应式系统详解

ref 和 reactive

Vue3 提供了两种创建响应式数据的方式:refreactive

import { ref, reactive, toRefs } from 'vue'

// ref 用于基本类型
const count = ref(0)
console.log(count.value) // 访问值需要 .value

// reactive 用于对象
const state = reactive({
  name: '张三',
  age: 25,
  skills: ['JavaScript', 'Vue', 'Node.js']
})

// 如果要把 reactive 对象展开为 ref
const { name, age } = toRefs(state)

提示: 在模板中使用时,ref 会自动解包,不需要写 .value

computed 计算属性

import { ref, computed } from 'vue'

const firstName = ref('张')
const lastName = ref('三')

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

// 可读可写的计算属性
const reversedName = computed({
  get() {
    return fullName.value.split('').reverse().join('')
  },
  set(value) {
    const [last, ...rest] = value.split('').reverse()
    lastName.value = last
    firstName.value = rest.reverse().join('')
  }
})

监听器 watch 和 watchEffect

Vue3 提供了两种监听方式,各有适用场景:

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

const count = ref(0)
const name = ref('')

// watch - 显式指定要监听的数据
watch(count, (newValue, oldValue) => {
  console.log(`count 从 ${oldValue} 变为 ${newValue}`)
}, { immediate: true })

// watch 多个数据源
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
  console.log(`变化: count=${newCount}, name=${newName}`)
})

// watchEffect - 自动收集依赖
watchEffect(() => {
  // 立即执行,并自动追踪函数中使用的所有响应式数据
  console.log(`自动追踪: count=${count.value}, name=${name.value}`)
})

生命周期钩子

Composition API 中的生命周期钩子需要从 vue 中显式导入:

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

export default {
  setup() {
    onMounted(() => {
      console.log('组件挂载完成')
    })
    
    onBeforeMount(() => {
      console.log('即将挂载')
    })
    
    onUnmounted(() => {
      console.log('组件即将卸载')
    })
    
    // 错误捕获
    onErrorCaptured((err, instance, info) => {
      console.error('捕获到错误:', err, info)
      return false // 阻止错误继续传播
    })
    
    return {}
  }
}

依赖注入:provide 和 inject

对于深层嵌套的组件,Props 层层传递会变得繁琐。使用 provide/inject 可以实现跨多层的组件通信:

// 父组件
import { provide, ref } from 'vue'

export default {
  setup() {
    const theme = ref('dark')
    const updateTheme = (newTheme) => {
      theme.value = newTheme
    }
    
    // 提供给所有后代组件
    provide('theme', theme)
    provide('updateTheme', updateTheme)
  }
}

// 子组件(任意层级)
import { inject } from 'vue'

export default {
  setup() {
    const theme = inject('theme')
    const updateTheme = inject('updateTheme')
    
    return { theme, updateTheme }
  }
}

Composables:组合式函数

Composables 是 Composition API 最强大的特性之一,允许我们将相关逻辑封装成可复用的函数:

// useMouse.js - 封装鼠标位置逻辑
import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)
  
  function update(e) {
    x.value = e.pageX
    y.value = e.pageY
  }
  
  onMounted(() => {
    window.addEventListener('mousemove', update)
  })
  
  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })
  
  return { x, y }
}

// useLocalStorage.js - 封装本地存储逻辑
export function useLocalStorage(key, defaultValue) {
  const data = ref(JSON.parse(localStorage.getItem(key) ?? JSON.stringify(defaultValue)))
  
  watch(data, (newValue) => {
    localStorage.setItem(key, JSON.stringify(newValue))
  }, { deep: true })
  
  return data
}

使用 Composable:

import { useMouse, useLocalStorage } from './composables'

export default {
  setup() {
    const { x, y } = useMouse()
    const username = useLocalStorage('username', '')
    
    return { x, y, username }
  }
}

实战案例:实现一个可复用的搜索组件

下面我们用 Composition API 实现一个功能完整的搜索组件:

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

export function useSearch(fetchFn, options = {}) {
  const { debounce = 300 } = options
  
  const query = ref('')
  const results = ref([])
  const loading = ref(false)
  const error = ref(null)
  
  let debounceTimer = null
  
  const performSearch = async () => {
    if (!query.value.trim()) {
      results.value = []
      return
    }
    
    loading.value = true
    error.value = null
    
    try {
      results.value = await fetchFn(query.value)
    } catch (e) {
      error.value = e.message
      results.value = []
    } finally {
      loading.value = false
    }
  }
  
  // 防抖处理
  watch(query, () => {
    clearTimeout(debounceTimer)
    debounceTimer = setTimeout(performSearch, debounce)
  })
  
  const hasResults = computed(() => results.value.length > 0)
  
  return {
    query,
    results,
    loading,
    error,
    hasResults,
    performSearch
  }
}

在组件中使用:

// SearchComponent.vue
import { useSearch } from './composables/useSearch'

export default {
  setup() {
    // 模拟搜索 API
    const searchAPI = async (keyword) => {
      const res = await fetch(`/api/search?q=${keyword}`)
      return res.json()
    }
    
    const { query, results, loading, error, hasResults } = useSearch(searchAPI, {
      debounce: 500
    })
    
    return { query, results, loading, error, hasResults }
  }
}

与 TypeScript 配合使用

Composition API 与 TypeScript 配合得非常好,以下是类型定义的示例:

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

// 定义接口
interface User {
  id: number
  name: string
  email: string
}

// 带类型的 ref
const count = ref<number>(0)
const user = ref<User | null>(null)

// 泛型计算属性
function useFilteredList<T>(list: Ref<T[]>, filter: Ref<(item: T) => boolean>) {
  return computed(() => list.value.filter(filter.value))
}

// Composable 的类型定义
interface UseCounterReturn {
  count: Ref<number>
  increment: () => void
  decrement: () => void
}

function useCounter(initial = 0): UseCounterReturn {
  const count = ref(initial)
  
  const increment = () => count.value++
  const decrement = () => count.value--
  
  return { count, increment, decrement }
}

性能优化技巧

使用 shallowRef 和 shallowReactive

对于大型数据结构,可以使用 shallowRef 和 shallowReactive 避免深层响应式开销:

import { shallowRef, shallowReactive } from 'vue'

// 只追踪顶层变化,内部变化不会触发更新
const state = shallowReactive({
  deep: {
    nested: {
      data: 'initial'
    }
  }
})

// shallowRef 只追踪 .value 的变化
const data = shallowRef({ large: 'structure' })

组件渲染优化

import { computed, readonly } from 'vue'

// 使用 readonly 防止外部修改
export function useConfig() {
  const config = reactive({
    apiUrl: 'https://api.example.com',
    timeout: 5000
  })
  
  // 只暴露只读版本
  return readonly(config)
}

// 冻结不需要响应式的大对象
const staticData = Object.freeze([
  { id: 1, label: '选项1' },
  { id: 2, label: '选项2' }
])

总结

Vue3 的 Composition API 为我们提供了一种更灵活、更强大的代码组织方式。通过掌握核心概念(ref、reactive、computed、watch)和最佳实践(Composables、依赖注入),我们可以构建出更易维护、更易测试的 Vue 应用。

建议在实际项目中逐步采用 Composition API,可以从新组件开始,逐步迁移旧组件,同时保持对新特性的学习和探索。

© 版权声明
THE END
喜欢就支持一下吧
点赞5 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片快捷回复

    暂无评论内容