Composition API 是 Vue 3 最重要的架构变革。它将组件的逻辑按"关注点"组织(而非按 Options API 的 data/computed/methods 分散),使复杂组件更易维护、逻辑更易复用。本文是 Vue Composition API 的完整参考手册。
一、<script setup>:Composition API 的语法糖
<script setup> 是 Vue 3.2+ 推荐的单文件组件写法,编译时自动将组件的逻辑转化为 setup() 函数。
<script setup>
// 所有变量和函数自动暴露给模板
import { ref } from 'vue'
const count = ref(0)
const increment = () => count.value++
</script>
<template>
<button @click="increment">{{ count }}</button>
</template>
等价于:
<script>
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
const increment = () => count.value++
return { count, increment }
},
}
</script>
二、ref 与 reactive:响应式基础
2.1 ref:值的响应式包装
import { ref } from 'vue'
const count = ref(0) // Ref<number>
const name = ref('Alice') // Ref<string>
const user = ref({ age: 25 }) // Ref<{ age: number }>
// 读取/修改都需要 .value
console.log(count.value) // 0
count.value++ // 触发更新
console.log(count.value) // 1
// 模板中自动解包,无需 .value
// <template><p>{{ count }}</p></template>
2.2 reactive:对象的深度响应式
import { reactive } from 'vue'
const state = reactive({
count: 0,
user: { name: 'Alice', age: 25 },
items: [],
})
state.count++ // 直接修改,无需 .value
state.user.name = 'Bob' // 深度响应式
state.items.push('apple') // 数组方法正常工作
2.3 ref vs reactive 选型表
| 场景 | 推荐 | 原因 |
|---|---|---|
| 基本类型 | ref | reactive 不支持基本类型 |
| 对象字面量 | reactive 或 ref | reactive 代码更简洁 |
| 需要解构赋值 | ref | reactive 解构会丢失响应性 |
| 作为函数返回值 | ref | 避免 reactive 的展开陷阱 |
| 表单数据 | reactive | 属性访问方便 |
| Props 传递 | ref | 包装值更安全 |
三、computed:计算属性
import { ref, computed } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
// 只读计算属性
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
// 可写计算属性
const displayName = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (val) => {
const [first, last] = val.split(' ')
firstName.value = first
lastName.value = last
},
})
// 使用
// displayName.value = 'Jane Smith' // 自动拆分赋值
computed vs method:
computed有缓存——依赖不变时不会重新计算method每次渲染都会执行- 需要缓存的结果用
computed,不需要缓存的操作用function
四、watch 与 watchEffect:侦听器
4.1 watch:显式追踪
import { ref, watch } from 'vue'
const query = ref('')
const results = ref([])
// 监听单个 ref
watch(query, async (newVal, oldVal) => {
results.value = await searchApi(newVal)
}, { immediate: false }) // immediate: 创建时立即执行一次
// 监听多个源
watch([query, page], async ([newQuery, newPage]) => {
results.value = await searchApi(newQuery, newPage)
})
// 监听深层对象(默认只监听引用变化)
watch(
() => state.user, // getter 返回要监听的对象
(newVal, oldVal) => { /* ... */ },
{ deep: true } // 深度监听
)
// 获取旧值(对象类型需注意结构问题)
watch(user, (newVal, oldVal) => {
console.log(`${oldVal?.name} → ${newVal?.name}`)
})
4.2 watchEffect:自动追踪
import { ref, watchEffect } from 'vue'
const count = ref(0)
const multiplier = ref(2)
// 自动追踪所有响应式依赖
watchEffect(() => {
console.log(count.value * multiplier.value) // 追踪 count 和 multiplier
})
// 带清理
watchEffect((onCleanup) => {
const timer = setInterval(() => {
console.log(count.value)
}, 1000)
onCleanup(() => {
clearInterval(timer) // 下一次执行或组件卸载时清理
})
})
watch vs watchEffect:
| 特性 | watch | watchEffect |
|---|---|---|
| 依赖追踪 | 显式声明 | 自动追踪 |
| 旧值访问 | ✅ 可以 | ❌ 不可以 |
| 立即执行 | 需要配置 immediate | ✅ 默认执行 |
| 适用 | 精确控制 | 副作用集成 |
五、生命周期钩子
import {
onBeforeMount, onMounted,
onBeforeUpdate, onUpdated,
onBeforeUnmount, onUnmounted,
onErrorCaptured,
} from 'vue'
// 挂载
onBeforeMount(() => console.log('组件挂载前'))
onMounted(() => console.log('组件已挂载'))
// 更新
onBeforeUpdate(() => console.log('组件更新前'))
onUpdated(() => console.log('组件已更新'))
// 卸载
onBeforeUnmount(() => console.log('组件卸载前'))
onUnmounted(() => console.log('组件已卸载'))
// 错误捕获
onErrorCaptured((err, instance, info) => {
console.error('捕获到错误:', err, info)
return false // true: 继续向上传播,false: 阻止传播
})
六、组件通信
6.1 Props + Emits
<!-- Parent.vue -->
<script setup>
import Child from './Child.vue'
const title = 'Hello'
const handleUpdate = (val) => console.log(val)
</script>
<template>
<Child :title="title" @update="handleUpdate" />
</template>
<!-- Child.vue -->
<script setup>
const props = defineProps({
title: { type: String, required: true },
count: { type: Number, default: 0 },
})
const emit = defineEmits(['update', 'delete'])
const notifyParent = () => {
emit('update', 'new value')
}
</script>
6.2 自定义 v-model
<!-- CustomInput.vue -->
<script setup>
const model = defineModel() // Vue 3.4+ 简化写法
// 等价于:const props = defineProps(['modelValue'])
// const emit = defineEmits(['update:modelValue'])
</script>
<template>
<input v-model="model" />
</template>
<!-- 使用 -->
<CustomInput v-model="text" />
6.3 provide / inject
// 祖先组件
import { provide, ref } from 'vue'
const theme = ref('dark')
provide('theme', theme)
provide('toggleTheme', () => {
theme.value = theme.value === 'dark' ? 'light' : 'dark'
})
// 后代组件
import { inject } from 'vue'
const theme = inject('theme', 'light') // 默认值 'light'
const toggleTheme = inject('toggleTheme')
七、自定义 Composable:逻辑复用
7.1 Composable 规范
// composables/useLocalStorage.ts
import { ref, watch } from 'vue'
export function useLocalStorage<T>(key: string, defaultValue: T) {
const stored = localStorage.getItem(key)
const data = ref<T>(stored ? JSON.parse(stored) : defaultValue)
watch(data, (newVal) => {
localStorage.setItem(key, JSON.stringify(newVal))
}, { deep: true })
return data // 返回 ref,方便解构
}
// 使用
const userPref = useLocalStorage('user-pref', { theme: 'dark', lang: 'zh' })
userPref.value.theme = 'light' // 自动同步到 localStorage
7.2 常用 Composable 示例
// useAsync.ts —— 异步数据获取
export function useAsync<T>(fn: () => Promise<T>) {
const data = ref<T | null>(null)
const error = ref<Error | null>(null)
const loading = ref(false)
const execute = async () => {
loading.value = true
error.value = null
try {
data.value = await fn()
} catch (e) {
error.value = e as Error
} finally {
loading.value = false
}
}
return { data, error, loading, execute }
}
// useEventListener.ts —— 事件监听
export function useEventListener(
target: EventTarget,
event: string,
callback: EventListener
) {
onMounted(() => target.addEventListener(event, callback))
onUnmounted(() => target.removeEventListener(event, callback))
}
八、完整组件示例
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
// ========== Props / Emits ==========
const props = defineProps<{
initialQuery: string
pageSize: number
}>()
const emit = defineEmits<{
search: [results: SearchResult[]]
}>()
// ========== State ==========
const query = ref(props.initialQuery)
const page = ref(1)
const results = ref<SearchResult[]>([])
const loading = ref(false)
const error = ref('')
// ========== Computed ==========
const hasNextPage = computed(() => results.value.length >= props.pageSize)
// ========== Methods ==========
async function fetchResults() {
loading.value = true
error.value = ''
try {
const data = await searchApi(query.value, page.value, props.pageSize)
results.value = data
emit('search', data)
} catch (e) {
error.value = (e as Error).message
} finally {
loading.value = false
}
}
function nextPage() {
page.value++
}
// ========== Watchers ==========
watch(query, () => {
page.value = 1 // 查询变化时重置页码
fetchResults()
}, { debounce: 300 }) // 防抖(需实现或使用第三方库)
watch(page, fetchResults)
// ========== Lifecycle ==========
onMounted(() => {
fetchResults()
})
</script>
<template>
<div class="search">
<input v-model="query" placeholder="Search..." />
<p v-if="loading">Loading...</p>
<p v-else-if="error" class="error">{{ error }}</p>
<ul v-else>
<li v-for="item in results" :key="item.id">{{ item.name }}</li>
</ul>
<button v-if="hasNextPage" @click="nextPage" :disabled="loading">
Next Page
</button>
</div>
</template>
常见问题(FAQ)
<script setup> 里怎么定义组件名?
<script setup>
defineOptions({ name: 'MyComponent' })
</script>
ref 和 reactive 可以混用吗?
可以。常见模式是用 reactive 管理局部状态,ref 管理需要传递或解构的值。
为什么 watch 监听 reactive 对象必须用 getter?
const state = reactive({ count: 0 })
// ❌ 无效:监听的是 state 的引用,不会触发
watch(state, () => {})
// ✅ 有效:getter 返回内部值
watch(() => state.count, (newVal) => {})
// ✅ 或用 deep: true
watch(() => state, handler, { deep: true })
相关阅读
- Vue 详解 — 核心概念与渐进式理念
- Vue Router 完全指南 — 路由守卫、懒加载
- Pinia 状态管理指南 — Vue 官方推荐的状态管理
- Vue + TypeScript 实战指南 — Composition API 的类型安全
- Vue 测试深度指南 — Vitest + Vue Test Utils
- Nuxt.js 完全指南 — 全栈 Vue 框架
- React 详解 — Vue 与 React 核心差异
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。
「frontend」更多文章
Vue 性能优化指南:虚拟列表、懒加载、渲染优化与 Core Web Vitals
Vue 3 应用性能优化完整策略:虚拟滚动(vue-virtual-scroller)、组件懒加载与异步组件、KeepAlive 缓存、Suspense 异步优化、v-memo 渲染记忆化、响应式性能(shallowRef/toRaw)、Bundle 分析与代码分割、渲染函数优化、以及 Core Web Vitals(LCP/INP/CLS)调优。
Nuxt.js 完全指南:Vue 全栈框架的 SSR、SSG、API 路由与部署实践
Nuxt.js 3 深度实践:文件系统路由、SSR/SSG/ISR 渲染模式、API 路由(Server Routes)、useFetch/useAsyncData 数据获取、中间件与插件、SEO 与 Meta 管理、Nitro 服务端引擎、自动导入、部署到 Vercel/Netlify/Node.js。
Vue 测试深度指南:Vitest + Vue Test Utils + Playwright E2E 与 CI 集成
Vue 3 应用从单元测试到 E2E 的完整测试策略:Vitest + Vue Test Utils 组件测试(mount/emits/slots/async)、Pinia Store Mocking、MSW API 拦截、Playwright 端到端测试、Cypress 组件测试、覆盖率标准与 GitHub Actions CI 集成。