返回首页 Vue3

Vue3 Composition API 完全指南:从入门到精通

Vue3 的 Composition API 是框架自发布以来最重要的特性之一。它不仅解决了 Options API 在复杂组件中的局限性,还为我们提供了更灵活、更可组合的代码组织方式。本文将深入探讨 Composition API 的核心概念、最佳实践以及高级技巧。


为什么需要 Composition API

在 Vue2 的 Options API 中,我们通过 datamethodscomputedwatch 等选项来组织代码。随着组件复杂度的增加,这种组织方式暴露出了一些问题:

Options API 的局限性

// 典型的 Options API 代码分散问题
export default {
  data() {
    return {
      user: null,
      posts: [],
      loading: false,
      searchQuery: '',
      // ... 更多状态
    }
  },
  computed: {
    filteredPosts() { /* ... */ },
    userDisplayName() { /* ... */ },
    // ... 更多计算属性
  },
  methods: {
    fetchUser() { /* ... */ },
    fetchPosts() { /* ... */ },
    handleSearch() { /* ... */ },
    // ... 更多方法
  },
  watch: {
    searchQuery() { /* ... */ },
    user() { /* ... */ },
    // ... 更多侦听器
  }
}

这种组织方式导致逻辑分散:同一个功能的代码被分散在不同选项中,难以阅读和维护。更糟糕的是,代码复用变得非常困难,只能通过 mixins 和高阶组件,但它们都有各自的问题。

Composition API 的优势

Composition API 让我们可以按照逻辑功能而不是选项类型来组织代码:

// Composition API:按功能组织代码
import { ref, computed, watch, onMounted } from 'vue'

export default {
  setup() {
    // 用户相关逻辑
    const user = ref(null)
    const userDisplayName = computed(() => user.value?.name ?? 'Guest')

    function fetchUser() {
      // 获取用户逻辑
    }

    // 文章相关逻辑
    const posts = ref([])
    const searchQuery = ref('')

    const filteredPosts = computed(() => {
      return posts.value.filter(post =>
        post.title.includes(searchQuery.value)
      )
    })

    function fetchPosts() {
      // 获取文章逻辑
    }

    watch(searchQuery, () => {
      // 搜索逻辑
    })

    onMounted(() => {
      fetchUser()
      fetchPosts()
    })

    return {
      user,
      userDisplayName,
      posts,
      searchQuery,
      filteredPosts
    }
  }
}
"Composition API 不是要替代 Options API,而是提供了另一种选择。对于简单组件,Options API 依然是很好的选择;但对于复杂组件,Composition API 能够显著提升代码的可维护性。"

setup 函数详解

setup 是 Composition API 的入口点。它在组件创建之前执行,是所有 Composition API 函数的使用场所。

基本用法

import { ref, computed } from 'vue'

export default {
  setup() {
    // 1. 定义响应式状态
    const count = ref(0)

    // 2. 定义计算属性
    const doubled = computed(() => count.value * 2)

    // 3. 定义方法
    function increment() {
      count.value++
    }

    // 4. 返回给模板使用
    return {
      count,
      doubled,
      increment
    }
  }
}

setup 参数

setup 函数接收两个参数:

export default {
  setup(props, context) {
    // props: 响应式的 props 对象
    console.log(props.message)

    // context: 包含三个可选属性
    context.attrs      // 非响应式的 attrs
    context.slots      // 非响应式的 slots
    context.emit       // 触发事件的方法
    context.expose     // 暴露公共属性(Vue 3.2+)

    // 解构写法
    const { attrs, slots, emit, expose } = context

    return {}
  }
}

注意:props 不能解构

// ❌ 错误:会丢失响应性
setup({ title }) {
  console.log(title) // 不会更新
}

// ✅ 正确:使用 toRefs
import { toRefs } from 'vue'

setup(props) {
  const { title } = toRefs(props)
  console.log(title.value) // 会更新
}

返回值与模板访问

// setup 返回对象
export default {
  setup() {
    return {
      message: 'Hello',
      count: ref(0)
    }
  },
  // 也可以与其他选项共存
  mounted() {
    console.log(this.count) // 可以访问
  }
}

// setup 返回渲染函数(罕见但强大)
import { h } from 'vue'

export default {
  setup() {
    return () => h('div', 'Hello World')
  }
}

响应式系统核心

Vue3 的响应式系统基于 ES6 Proxy 实现,提供了比 Vue2 的 Object.defineProperty 更强大的能力。

ref vs reactive

这是使用最广泛的两个响应式 API,它们的区别非常重要:

特性 ref reactive
适用类型 基本类型、对象 仅对象
访问方式 .value 直接访问
模板解包 自动 不需要
重新赋值 支持 不支持

ref 详解

import { ref } from 'vue'

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

// 对象类型
const user = ref({ name: 'Alice' })
console.log(user.value.name) // 'Alice'

// 模板中自动解包
// <template>
//   <div>{{ count }}</div>  // 不需要 .value
//   <div>{{ user.name }}</div>  // 自动解包
// </template>

// 数组
const items = ref([])
items.value.push({ id: 1 }) // 需要通过 .value

reactive 详解

import { reactive } from 'vue'

// 对象
const state = reactive({
  count: 0,
  user: { name: 'Alice' }
})

// 访问和修改
state.count++
state.user.name = 'Bob'

// 嵌套对象也是响应式的
// 数组
const list = reactive([{ id: 1 }])
list.push({ id: 2 })

// ⚠️ 注意:不能重新赋值
state = reactive({ count: 1 }) // ❌ 不响应!
// 解决方案:使用 ref 或 Object.assign

toRef 和 toRefs

这两个函数在处理响应式对象时非常有用:

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

const state = reactive({
  count: 0,
  message: 'Hello'
})

// toRef: 创建单个 ref
const countRef = toRef(state, 'count')
countRef.value++ // state.count 也会更新

// toRefs: 将所有属性转为 ref
const { count, message } = toRefs(state)
count.value++
message.value = 'Hi'
// state 也会更新

// 典型用法:在 setup 中返回
setup() {
  const state = reactive({
    count: 0,
    name: 'Vue'
  })

  return {
    ...toRefs(state) // 解构时保持响应性
  }
}

readonly 和 shallowReadonly

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

const original = reactive({ count: 0 })

// 深度只读
const copy = readonly(original)
copy.count++ // ⚠️ 警告:不能修改只读属性

// 浅层只读(深层可修改)
const shallow = shallowReadonly({
  nested: { count: 0 }
})
shallow.nested.count++ // ✅ 可以修改深层属性
shallow.other = 'new' // ❌ 不能添加顶层属性

toRaw 和 markRaw

import { reactive, toRaw, markRaw } from 'vue'

const obj = reactive({ count: 0 })

// toRaw: 获取原始对象
const raw = toRaw(obj)
raw.count++ // 不会触发响应式更新

// markRaw: 标记对象永远不需要响应式
const thirdPartyLib = markRaw({
  method() {
    // 第三方库的方法
  }
})

const state = reactive({
  lib: thirdPartyLib // 不会转换为响应式
})

计算属性与侦听器

computed 计算属性

import { ref, computed } from 'vue'

const count = ref(0)

// 只读计算属性
const doubled = computed(() => count.value * 2)

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

// 计算属性缓存
// computed 会缓存结果,只有依赖变化才重新计算
const expensive = computed(() => {
  console.log('computing...')
  return heavyCalculation()
})
expensive.value // 'computing...'
expensive.value // 使用缓存,不输出

watch 侦听器

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

const count = ref(0)
const user = ref({ name: 'Alice' })

// 侦听单个 ref
watch(count, (newValue, oldValue) => {
  console.log(`count: ${oldValue} -> ${newValue}`)
})

// 侦听多个源
watch([count, user], ([newCount, newUser], [oldCount, oldUser]) => {
  console.log('multiple changed')
})

// 侦听 getter 函数
watch(
  () => user.value.name,
  (newName, oldName) => {
    console.log(`name: ${oldName} -> ${newName}`)
  }
)

// 深度侦听
watch(user, (newVal, oldVal) => {
  console.log('user changed')
}, { deep: true })

// 立即执行
watch(count, (val) => {
  console.log(val)
}, { immediate: true })

watchEffect 自动追踪

import { watchEffect } from 'vue'

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

// 自动追踪依赖
watchEffect(() => {
  console.log(`${name.value}: ${count.value}`)
})
// 每当 count 或 name 变化时自动执行

// 停止侦听
const stop = watchEffect(() => {
  // ...
})
stop() // 手动停止

// 副作用刷新时机
watchEffect(
  () => {
    // 副作用代码
  },
  {
    flush: 'post'   // 'pre' | 'post' | 'sync'
    // pre: 组件更新前(默认)
    // post: 组件更新后
    // sync: 同步执行
  }
)

// onTrack 和 onTrigger 调试
watchEffect(
  () => {
    console.log(count.value)
  },
  {
    onTrack(e) {
      console.log('tracking', e)
    },
    onTrigger(e) {
      console.log('triggered', e)
    }
  }
)

生命周期钩子

Composition API 使用 onX 格式的生命周期钩子:

Options API Composition API
beforeCreate setup()
created setup()
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmounted onUnmounted
errorCaptured onErrorCaptured
renderTracked onRenderTracked
renderTriggered onRenderTriggered
import {
  onMounted,
  onUpdated,
  onUnmounted,
  onBeforeMount
} from 'vue'

export default {
  setup() {
    onMounted(() => {
      console.log('组件已挂载')
    })

    onBeforeMount(() => {
      console.log('即将挂载')
    })

    onUpdated(() => {
      console.log('组件已更新')
    })

    onUnmounted(() => {
      console.log('组件已卸载')
    })

    // 可以多次调用
    onMounted(() => {
      console.log('另一个 mounted 钩子')
    })
  }
}

组合式函数模式

组合式函数(Composables)是复用逻辑的最佳方式。它本质上是一个返回响应式状态的函数:

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

使用组合式函数

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

export default {
  setup() {
    const { x, y } = useMouse()

    return { x, y }
  }
}

// 在 <template> 中
// <div>Mouse: {{ x }}, {{ y }}</div>

更多 Composable 示例

// composables/useFetch.js
import { ref, watchEffect } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)

  watchEffect(async () => {
    data.value = null
    error.value = null

    try {
      const res = await fetch(url.value)
      data.value = await res.json()
    } catch (err) {
      error.value = err
    }
  })

  return { data, error }
}

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

export function useLocalStorage(key, defaultValue) {
  const stored = localStorage.getItem(key)
  const value = ref(stored ? JSON.parse(stored) : defaultValue)

  watch(value, () => {
    localStorage.setItem(key, JSON.stringify(value.value))
  }, { deep: true })

  return value
}

// 使用
const theme = useLocalStorage('theme', 'light')
const { data, error } = useFetch(ref('/api/users'))

TypeScript 最佳实践

Vue3 + TypeScript 是绝配。Composition API 让类型推断变得更加准确:

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

// 定义类型
interface User {
  id: number
  name: string
  email: string
}

// ref 会自动推断类型
const count = ref(0) // Ref<number>

// 也可以显式指定
const user: Ref<User | null> = ref(null)

// reactive 需要类型注解
const state: {
  users: User[]
  loading: boolean
} = reactive({
  users: [],
  loading: false
})

// 计算属性类型推断
const userName = computed(() => user.value?.name ?? '')
// Ref<string>

// 组合式函数类型
function useUserApi() {
  const users = ref<User[]>([])
  const loading = ref(false)

  async function fetchUsers(): Promise<void> {
    loading.value = true
    const res = await fetch('/api/users')
    users.value = await res.json()
    loading.value = false
  }

  return {
    users,
    loading,
    fetchUsers
  }
}

常见设计模式

1. 状态提取

// store/useTodoStore.js
import { reactive, computed } from 'vue'

const state = reactive({
  todos: [],
  filter: 'all'
})

export function useTodoStore() {
  const filtered = computed(() => {
    if (state.filter === 'active') {
      return state.todos.filter(t => !t.done)
    }
    if (state.filter === 'done') {
      return state.todos.filter(t => t.done)
    }
    return state.todos
  })

  function addTodo(text) {
    state.todos.push({
      id: Date.now(),
      text,
      done: false
    })
  }

  return {
    state,
    filtered,
    addTodo
  }
}

2. 依赖注入

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

export default {
  setup() {
    const theme = ref('dark')
    provide('theme', theme)
  }
}

// 子组件
import { inject } from 'vue'

export default {
  setup() {
    const theme = inject('theme', 'light') // 默认值
    return { theme }
  }
}

3. 异步操作

function useAsync(asyncFn) {
  const state = reactive({
    data: null,
    error: null,
    loading: false
  })

  async function execute(...args) {
    state.loading = true
    state.error = null

    try {
      state.data = await asyncFn(...args)
    } catch (error) {
      state.error = error
    } finally {
      state.loading = false
    }
  }

  return {
    ...toRefs(state),
    execute
  }
}

总结

Vue3 Composition API 提供了一套强大而灵活的工具集,让我们能够更好地组织代码、复用逻辑。掌握好这些核心概念和模式,将帮助你构建更加可维护的应用程序。

关键要点