Vue 3 组合式 API 完全指南:从入门到实战

为什么选择组合式 API?

Vue 3 引入的组合式 API(Composition API)是 Vue 框架最重要的进化之一。相比传统的选项式 API,组合式 API 提供了更好的代码组织方式,特别是在处理复杂组件逻辑时。

在选项式 API 中,一个功能的逻辑被分散在 datacomputedmethodswatch 等不同选项中。当组件变大时,维护变得困难。组合式 API 允许我们按功能组织代码,相关逻辑集中在一起。

核心响应式 API

ref —— 基本类型的响应式

ref 用于创建一个响应式的引用,通常用于基本类型(string、number、boolean)。它返回一个带有 .value 属性的包装对象。

import { ref } from 'vue'

// 创建响应式引用
const count = ref(0)
const message = ref('Hello Vue 3')
const isVisible = ref(true)

// 在 JavaScript 中通过 .value 访问
console.log(count.value) // 0
count.value++
console.log(count.value) // 1

// 在模板中自动解包,无需 .value
// <template>
//   <p>{{ count }}</p>  <!-- 直接使用,无需 .value -->
// </template>

reactive —— 对象类型的响应式

reactive 用于创建一个深层响应式的对象,适合处理复杂的数据结构。

import { reactive } from 'vue'

const user = reactive({
  name: '博豪',
  age: 25,
  skills: ['JavaScript', 'Vue', 'Node.js'],
  profile: {
    github: 'https://github.com/bohao',
    blog: 'taptapminigame.cloud'
  }
})

// 直接修改属性,无需 .value
user.name = '陈博豪'
user.skills.push('TypeScript')
user.profile.github = 'https://github.com/new-url'

何时用 ref,何时用 reactive? 一般来说,基本类型用 ref,对象类型用 reactive。但实际开发中,很多开发者统一使用 ref,因为它在赋值时更直观(不会丢失响应性)。

computed —— 计算属性

computed 用于创建基于其他响应式状态的派生值,具有缓存特性。

import { ref, computed } from 'vue'

const firstName = ref('陈')
const lastName = ref('博豪')

// 只读计算属性
const fullName = computed(() => firstName.value + lastName.value)

// 可写计算属性
const fullNameWritable = computed({
  get: () => firstName.value + lastName.value,
  set: (val) => {
    firstName.value = val.charAt(0)
    lastName.value = val.slice(1)
  }
})

// 带缓存:只有当 firstName 或 lastName 变化时才重新计算
console.log(fullName.value) // '陈博豪'

watch 与 watchEffect —— 侦听器

Vue 3 提供了两种侦听响应式数据变化的方式。

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

const keyword = ref('')
const page = ref(1)

// watch: 明确指定要侦听的数据
watch(keyword, (newVal, oldVal) => {
  console.log(`搜索词从 "${oldVal}" 变为 "${newVal}"`)
  page.value = 1 // 关键词变化时重置页码
})

// watch 多个源
watch([keyword, page], ([newKeyword, newPage]) => {
  fetchData(newKeyword, newPage)
})

// watchEffect: 自动追踪依赖
watchEffect(() => {
  // 自动追踪内部用到的所有响应式数据
  console.log(`当前搜索: ${keyword.value}, 第 ${page.value} 页`)
  fetchData(keyword.value, page.value)
})

生命周期钩子

组合式 API 中的生命周期钩子以 on 为前缀,需要在 setup() 中调用。

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

// <script setup> 中直接使用
onMounted(() => {
  console.log('组件已挂载')
  // 适合:发起 API 请求、操作 DOM、初始化第三方库
})

onUnmounted(() => {
  console.log('组件已卸载')
  // 适合:清理定时器、取消事件监听、断开 WebSocket
})

<script setup> 语法糖

<script setup> 是 Vue 3.2 引入的编译时语法糖,让组合式 API 的使用更加简洁。

<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import UserCard from './UserCard.vue'

// 所有的顶层变量、函数、导入都会自动暴露给模板
const count = ref(0)
const doubled = computed(() => count.value * 2)
const router = useRouter()

function increment() {
  count.value++
}

onMounted(() => {
  console.log('组件已挂载')
})
</script>

<template>
  <div>
    <p>{{ count }} × 2 = {{ doubled }}</p>
    <button @click="increment">+1</button>
    <UserCard />
  </div>
</template>

相比传统的 setup() 函数,<script setup> 的优势:

实战:构建一个可复用的搜索组件

下面通过一个完整的实例来展示组合式 API 的实际应用。我们将构建一个带防抖的搜索组件。

第一步:创建 useDebounce 组合式函数

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

export function useDebounce(value, delay = 300) {
  const debouncedValue = ref(value.value)
  let timer = null

  watch(value, (newVal) => {
    clearTimeout(timer)
    timer = setTimeout(() => {
      debouncedValue.value = newVal
    }, delay)
  })

  return debouncedValue
}

第二步:创建 useFetch 组合式函数

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

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

  async function fetchData() {
    loading.value = true
    error.value = null
    try {
      const res = await fetch(url.value || url)
      if (!res.ok) throw new Error(`HTTP ${res.status}`)
      data.value = await res.json()
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }

  if (typeof url === 'object' && url.value !== undefined) {
    watchEffect(fetchData)
  } else {
    fetchData()
  }

  return { data, error, loading, refetch: fetchData }
}

第三步:组装搜索组件

<script setup>
import { ref, computed } from 'vue'
import { useDebounce } from '../composables/useDebounce'
import { useFetch } from '../composables/useFetch'

const keyword = ref('')
const debouncedKeyword = useDebounce(keyword, 500)

const apiUrl = computed(
  () => `/api/search?q=${encodeURIComponent(debouncedKeyword.value)}`
)

const { data: results, loading, error } = useFetch(apiUrl)
</script>

<template>
  <div class="search-component">
    <input v-model="keyword" placeholder="输入搜索关键词..." />
    <div v-if="loading" class="loading">搜索中...</div>
    <div v-else-if="error" class="error">{{ error }}</div>
    <ul v-else-if="results?.length">
      <li v-for="item in results" :key="item.id">
        {{ item.title }}
      </li>
    </ul>
    <p v-else-if="debouncedKeyword">没有找到相关结果</p>
  </div>
</template>

最佳实践总结

import { toRefs } from 'vue'

function useUser() {
  const state = reactive({ name: '', age: 0 })
  // 解构时保持响应性
  return { ...toRefs(state) }
}

const { name, age } = useUser() // name 和 age 都是 ref

结语

Vue 3 的组合式 API 不仅是一种新的语法,更是一种全新的代码组织思维。它让逻辑复用变得更加自然,让大型组件的维护变得更加轻松。建议在新项目中优先使用 <script setup> + 组合式 API 的方式来构建组件。

如果你正在从 Vue 2 迁移,不必急于全面重构,可以在新组件中使用组合式 API,逐步过渡。Vue 3 完全兼容选项式 API,两种风格可以在同一个项目中共存。