Vue 3 Composition API 深度指南

2026.07.22· 约 1 分钟 ·tech·
#Vue
#Composition API
#前端

为什么需要 Composition API?

Vue 2 时代,我们写组件依赖 Options API——把 data、methods、computed、watch 分别塞进不同选项里。小型组件还好,但当组件逻辑变复杂时,同一个功能的代码被拆散到不同选项中,阅读和维护都很痛苦。Vue 3 引入的 Composition API 彻底改变了这一点:按逻辑关注点组织代码,而非按选项类型。

打个比方:Options API 像是把所有忍术卷轴分别放在「火遁架」「水遁架」「雷遁架」上——你要修改一个完整的忍术,就得翻好几个架子。Composition API 则把每个忍术的教学内容收在一卷里,翻阅起来一气呵成。

ref vs reactive:谁是你的影分身?

ref 和 reactive 是 Composition API 中最常用的两个响应式 API,但很多初学者搞不清什么时候用哪个。

ref:单值包装器

import { ref } from 'vue'

const count = ref(0)
const name = ref('Naruto')
const isVisible = ref(true)

// 在 script 中访问需要 .value
console.log(count.value) // 0

// 在 template 中自动解包
// <span>{{ count }}</span>

ref 适合包装基本类型(string、number、boolean),但也可以包装对象。内部原理是用一个带有 .value 属性的响应式对象包装值。

reactive:对象代理

import { reactive } from 'vue'

const state = reactive({
  user: { name: 'Naruto', level: 99 },
  missions: ['S级任务', 'A级任务']
})

// 不需要 .value,直接访问
console.log(state.user.name) // 'Naruto'

reactive 通过 Proxy 代理整个对象,适合管理一组相关的状态。但它有几个限制:

⚠️ 注意: reactive 不能包装基本类型;解构会丢失响应式;不能整体替换(state = newState 不会触发更新)。

实战选择策略

┌─────────────────────────────────────┐
│  简单值(string, number, bool)  → ref     │
│  单条数据(article, user)       → ref     │
│  一组紧密耦合的状态              → reactive │
│  不确定的时候                    → ref     │  ← 最安全的选择
└─────────────────────────────────────┘

computed 和 watch:感知查克拉的变化

computed:派生状态

import { ref, computed } from 'vue'

const missions = ref([
  { rank: 'S', reward: 10000 },
  { rank: 'A', reward: 5000 },
  { rank: 'B', reward: 1000 }
])

// 自动追踪依赖,只在 missions 变化时重新计算
const totalReward = computed(() =>
  missions.value.reduce((sum, m) => sum + m.reward, 0)
)

const sRankMissions = computed(() =>
  missions.value.filter(m => m.rank === 'S')
)

computed 有缓存机制——依赖不变时直接返回缓存值,不会重复计算。别把它和普通函数搞混:模板里多次引用 totalReward,只会执行一次计算函数。

watch:副作用侦探

import { ref, watch } from 'vue'

const searchQuery = ref('')
const results = ref([])

// 基础用法:监听 ref 变化
watch(searchQuery, async (newQuery, oldQuery) => {
  if (newQuery.trim()) {
    results.value = await fetchSearchResults(newQuery)
  }
})

// 监听多个源
watch([searchQuery, filterOptions], ([newQ, newFilter]) => {
  console.log('搜索条件变动')
})

// 深度监听 reactive 对象
watch(state, (newVal) => {
  console.log('状态发生了深层变化')
}, { deep: true })

// 立即执行 + 防抖
watch(searchQuery, () => {
  debouncedSearch()
}, { immediate: true })

watchEffect:更懒的方式

import { watchEffect } from 'vue'

// 自动追踪内部用到的所有响应式依赖
watchEffect(() => {
  // 这里面用到什么 ref / reactive,就自动监听什么
  document.title = `${pageTitle.value} - 木葉忍村`
})

选择原则:知道要监听什么用 watch,只关心副作用结果用 watchEffect。

可组合函数(Composables):写一次,到处用

这可能是 Composition API 最强大的特性。把逻辑提取成可复用的函数,无需 mixin、无需高阶组件。

实战:useMouse

// composables/useMouse.ts
import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)

  function update(event: MouseEvent) {
    x.value = event.pageX
    y.value = event.pageY
  }

  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  return { x, y }
}

在任何组件中直接用:

<script setup>
const { x, y } = useMouse()
</script>

<template>
  <p>鼠标位置:{{ x }}, {{ y }}</p>
</template>

实战:useFetch

import { ref } from 'vue'

export function useFetch<T>(url: string) {
  const data = ref<T | null>(null)
  const error = ref<Error | null>(null)
  const loading = ref(false)

  async function execute() {
    loading.value = true
    error.value = null
    try {
      const res = await fetch(url)
      data.value = await res.json()
    } catch (e) {
      error.value = e as Error
    } finally {
      loading.value = false
    }
  }

  execute()
  return { data, error, loading, refetch: execute }
}

💡 命名约定: 可组合函数必须以 use 开头(useMouse、useFetch、useDark),这是 Vue 社区的铁律,IDE 和 ESLint 插件都依赖这个约定。

<script setup>:省去样板代码的糖

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'

// ✅ 不需要 defineComponent
// ✅ 不需要 return { count, double }
// ✅ 顶层绑定自动暴露给模板

const count = ref(0)
const double = computed(() => count.value * 2)

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

// Props 和 Emits 用编译器宏
const props = defineProps<{ title: string }>()
const emit = defineEmits<{ update: [value: number] }>()
</script>

<template>
  <h1>{{ title }}</h1>
  <p>{{ count }} × 2 = {{ double }}</p>
  <button @click="count++">+1</button>
</template>

不需要手动 import { defineProps },它是编译器宏,构建时自动被处理。

从 Options API 迁移的对照表

Options APIComposition API (<script setup>)
data() { return { x: 1 } }const x = ref(1)
computed: { d() { return ... } }const d = computed(() => ...)
watch: { x() { ... } }watch(x, (val) => { ... })
methods: { fn() {} }function fn() {}
mounted() {}onMounted(() => {})
props: ['title']const props = defineProps<{ title: string }>()
this.$emit('e', v)const emit = defineEmits<{ e: [v: number] }>() → emit('e', v)
provide: { key: val }provide('key', val)
inject: ['key']const val = inject('key')

🎯 核心心得: 别试图一次性把整个项目从 Options API 重构成 Composition API。新组件用新方式写,旧组件在需要改逻辑时顺手迁移,这才是务实之道。

总结

Composition API 不是 Vue 3 的"新写法",而是更好的逻辑组织方式。它让代码更容易拆分和复用——你可以把关联逻辑打包成 composable,在组件间像积木一样拼装。这也是为什么 Vue 3 社区生态能在短时间内涌现出 VueUse 这样的高质量工具库。

掌握了这些基础,你已经可以写出一手干净的 Vue 3 代码了。接下来,去实战吧,忍者!

🔗相关忍术

00:00:00
系统已稳定运行:
Nuxt 3Vue 3Tailwind 3
粤ICP备2026111544号
🍥木葉忍村影分身之术!在代码、设计与写作间穿梭的技术忍者。这里记录着技...

© 2025-2026 木葉忍村 · Powered by Nuxt 3 & Tailwind CSS