引言:为何选择Composition API?
随着Vue 3的正式发布,官方在框架设计上进行了重大革新,其中最核心的变化之一就是引入了Composition API。它取代了以往以Options API为主的开发模式,为组件逻辑组织带来了前所未有的灵活性和可复用性。
在传统 Options API 中,组件的逻辑被分散在 data、methods、computed、watch 等选项中,当组件复杂度上升时,同一业务逻辑可能跨越多个区域,难以维护。而 Composition API 通过函数式的方式将相关逻辑集中管理,实现了“按功能组织代码”(Organize by Feature),显著提升了代码可读性和可维护性。
此外,Composition API 还提供了更强大的类型支持(尤其在 TypeScript 中)、更好的逻辑复用能力(通过 Composables)、以及对响应式系统的深度控制。本文将带你从零开始,深入掌握 Vue 3 Composition API 的完整开发流程与最佳实践。
✅ 适用人群:熟悉 Vue 2 或有一定前端经验的开发者
✅ 技术栈要求:现代浏览器 + ES6+ 支持 + TypeScript(推荐)
✅ 推荐开发环境:Vite + Vue CLI + VS Code + Volar 插件
一、基础概念:什么是 Composition API?
1.1 定义与核心思想
Composition API 是一组基于函数的 API,允许你在组件内部使用函数来组织和封装逻辑。它的核心理念是:
将相关的逻辑聚合在一起,而不是按照选项分类。
例如,在一个用户信息表单组件中,你可能会有:
- 表单数据绑定
- 表单验证规则
- 提交逻辑
- 加载状态管理
- 错误提示处理
在 Options API 中,这些逻辑会被拆分到不同的选项块中;而在 Composition API 中,你可以创建一个 useUserForm() 函数,将所有相关内容集中封装。
1.2 主要组成模块
| 模块 | 功能 |
|---|---|
setup() |
组合逻辑的入口函数(Vue 3 核心入口) |
ref() |
声明响应式基本类型(如字符串、数字) |
reactive() |
声明响应式对象(深度监听) |
computed() |
定义计算属性 |
watch() |
监听响应式数据变化 |
onMounted / onUnmounted 等 |
生命周期钩子 |
defineProps / defineEmits |
用于 <script setup> 的声明语法糖 |
⚠️ 注意:
<script setup>是 Vue 3 推荐的新语法,它让 Composition API 更简洁高效。
二、核心工具详解:响应式系统与数据管理
2.1 ref():处理基本类型的响应式
ref() 用于包装一个原始值,使其变为响应式。虽然它返回的是一个对象(包含 .value 属性),但可以在模板中直接使用。
<!-- UserCard.vue -->
<script setup>
import { ref } from 'vue'
const username = ref('Alice')
const age = ref(25)
// 可以直接在模板中使用
// {{ username }} → "Alice"
// {{ age }} → 25
// 也可以修改
function updateName() {
username.value = 'Bob'
}
</script>
<template>
<div>
<p>用户名: {{ username }}</p>
<p>年龄: {{ age }}</p>
<button @click="updateName">改名</button>
</div>
</template>
✅ 最佳实践:
- 使用
ref包装布尔值、字符串、数字等基本类型。 - 在模板中访问时无需
.value,Vue 会自动解包。 - 避免在
ref内部嵌套深层对象(应使用reactive)。
2.2 reactive():创建响应式对象
reactive() 用于创建一个响应式的对象或数组,其内部属性都会被追踪。
<script setup>
import { reactive } from 'vue'
const user = reactive({
name: 'Charlie',
email: 'charlie@example.com',
hobbies: ['reading', 'coding']
})
function addHobby(hobby) {
user.hobbies.push(hobby)
}
function updateEmail(newEmail) {
user.email = newEmail
}
</script>
<template>
<div>
<p>姓名: {{ user.name }}</p>
<p>邮箱: {{ user.email }}</p>
<ul>
<li v-for="h in user.hobbies" :key="h">{{ h }}</li>
</ul>
<button @click="addHobby('gaming')">添加爱好</button>
</div>
</template>
⚠️ 注意事项:
reactive()不能用于基本类型(必须传入对象或数组)。- 不支持顶层
undefined/null赋值。 - 如果你需要将
reactive对象作为props传递给子组件,需注意引用共享问题。
2.3 toRefs() 与 toRef():解构响应式对象不丢失响应性
当你从 reactive() 创建的对象中解构属性时,响应性会丢失。解决方法是使用 toRefs()。
<script setup>
import { reactive, toRefs } from 'vue'
const state = reactive({
count: 0,
message: 'Hello'
})
// 正确方式:使用 toRefs
const { count, message } = toRefs(state)
// ❌ 错误方式:直接解构
// const { count, message } = state // 响应性丢失!
function increment() {
count.value += 1
}
</script>
📌
toRefs()将响应式对象的所有属性转换为ref,保持响应性。
如果只需要某个特定属性转成 ref,可用 toRef():
const countRef = toRef(state, 'count') // 等价于 ref(state.count)
✅ 最佳实践:
- 在组合逻辑中需要解构响应式对象时,优先使用
toRefs。 - 适用于
setup()返回对象给模板的情况。
三、组合逻辑封装:Composables 模式
3.1 什么是 Composable?
Composable 是一个命名规范化的函数,用于封装可复用的逻辑单元。通常以 useXXX 开头命名,如 useLocalStorage、useFetch、useCounter。
这类函数可以被多个组件调用,实现跨组件逻辑复用。
3.2 实战案例:封装一个本地存储持久化工具
// composables/useLocalStorage.js
export function useLocalStorage(key, initialValue) {
const storedValue = localStorage.getItem(key)
const value = storedValue ? JSON.parse(storedValue) : initialValue
const state = ref(value)
// 监听变化并同步到 localStorage
watch(
state,
(newVal) => {
localStorage.setItem(key, JSON.stringify(newVal))
},
{ deep: true }
)
return state
}
应用示例:
<!-- ProfileSettings.vue -->
<script setup>
import { useLocalStorage } from '@/composables/useLocalStorage'
const theme = useLocalStorage('user-theme', 'light')
const preferences = useLocalStorage('user-preferences', { notifications: true })
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
</script>
<template>
<div>
<p>当前主题: {{ theme }}</p>
<button @click="toggleTheme">切换主题</button>
<p>通知开启: {{ preferences.notifications }}</p>
</div>
</template>
✅ 优势:
- 逻辑独立,易于测试
- 多组件共享相同行为
- 类型安全(配合 TypeScript)
3.3 通用 Composable 模板结构
// composables/useXXX.js
import { ref, computed, watch } from 'vue'
export function useXXX(initialValue) {
// 1. 声明状态
const state = ref(initialValue)
// 2. 计算属性(可选)
const formattedValue = computed(() => {
return state.value.toUpperCase()
})
// 3. 事件处理函数
const update = (newValue) => {
state.value = newValue
}
// 4. 监听器(可选)
watch(state, (newVal, oldVal) => {
console.log(`Changed from ${oldVal} to ${newVal}`)
})
// 5. 返回接口
return {
state,
formattedValue,
update
}
}
💡 建议:每个 Composable 应该职责单一,避免过度耦合。
四、生命周期钩子:从 setup() 到 onXxx
4.1 传统生命周期对比
| Options API | Composition API |
|---|---|
beforeCreate |
无(已在 setup() 执行前) |
created |
无(同上) |
beforeMount |
onBeforeMount |
mounted |
onMounted |
beforeUpdate |
onBeforeUpdate |
updated |
onUpdated |
beforeUnmount |
onBeforeUnmount |
unmounted |
onUnmounted |
⚠️
setup()执行时机相当于beforeCreate+created,所以无法再使用this。
4.2 详细示例:加载动画与资源清理
<script setup>
import { onMounted, onBeforeUnmount, ref } from 'vue'
const loading = ref(true)
const data = ref(null)
// 模拟异步请求
onMounted(async () => {
try {
const res = await fetch('/api/user')
data.value = await res.json()
} catch (err) {
console.error('Failed to load:', err)
} finally {
loading.value = false
}
})
// 清理定时器或其他副作用
let timerId
onBeforeUnmount(() => {
if (timerId) {
clearInterval(timerId)
}
})
// 模拟后台任务
onMounted(() => {
timerId = setInterval(() => {
console.log('Keep alive...')
}, 5000)
})
</script>
<template>
<div v-if="loading">加载中...</div>
<div v-else>{{ data }}</div>
</template>
✅ 最佳实践:
- 所有副作用(网络请求、定时器、订阅事件)应在
onMounted中启动。 - 在
onBeforeUnmount中进行清理,防止内存泄漏。 - 避免在
setup()内部执行副作用。
五、事件通信:defineProps 与 defineEmits
5.1 <script setup> 中的声明语法糖
defineProps 和 defineEmits 是 Vue 3 提供的编译时宏,可在 <script setup> 中直接使用,无需手动定义。
<!-- TodoItem.vue -->
<script setup>
import { defineProps, defineEmits } from 'vue'
const props = defineProps({
todo: {
type: Object,
required: true
}
})
const emit = defineEmits(['remove', 'toggle'])
function handleRemove() {
emit('remove', props.todo.id)
}
function handleToggle() {
emit('toggle', props.todo.id)
}
</script>
<template>
<li>
<span :class="{ completed: todo.completed }">
{{ todo.text }}
</span>
<button @click="handleToggle">Toggle</button>
<button @click="handleRemove">Delete</button>
</li>
</template>
5.2 与父组件通信
<!-- TodoList.vue -->
<script setup>
import { ref } from 'vue'
import TodoItem from './TodoItem.vue'
const todos = ref([
{ id: 1, text: '学习 Vue 3', completed: false },
{ id: 2, text: '写技术文章', completed: true }
])
function removeTodo(id) {
todos.value = todos.value.filter(t => t.id !== id)
}
function toggleTodo(id) {
todos.value = todos.value.map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
)
}
</script>
<template>
<ul>
<TodoItem
v-for="todo in todos"
:key="todo.id"
:todo="todo"
@remove="removeTodo"
@toggle="toggleTodo"
/>
</ul>
</template>
✅ 最佳实践:
- 使用
defineProps明确声明输入类型(尤其在 TypeScript 项目中)。 - 使用
defineEmits声明事件名称,便于 IDE 提示与校验。 - 避免使用
$emit,除非在非<script setup>场景。
六、高级特性:watch 与 computed 的深度应用
6.1 watch:灵活的数据监听机制
6.1.1 监听单个响应式变量
const count = ref(0)
watch(count, (newVal, oldVal) => {
console.log(`Count changed from ${oldVal} to ${newVal}`)
})
6.1.2 监听多个响应式变量
const firstName = ref('')
const lastName = ref('')
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
console.log(`Name updated: ${newFirst} ${newLast}`)
})
6.1.3 监听对象属性变化(深度监听)
const user = reactive({ name: '', age: 0 })
watch(
() => user.name,
(newName) => {
console.log('Name changed:', newName)
},
{ immediate: true } // 立即执行一次
)
✅
immediate: true用于立即触发一次回调。
6.1.4 监听复杂表达式
watch(
() => user.age > 18,
(isAdult) => {
if (isAdult) {
alert('已成年!')
}
}
)
6.1.5 使用 watchEffect —— 自动推导依赖
watchEffect 会自动收集依赖项,无需显式指定。
const count = ref(0)
const double = computed(() => count.value * 2)
watchEffect(() => {
console.log(`Double is: ${double.value}`)
})
// → 每次 count 变化时自动打印
✅ 适合不需要精确控制依赖项的场景,如日志记录、副作用操作。
6.2 computed:高效的缓存计算属性
<script setup>
import { ref, computed } from 'vue'
const first = ref('')
const last = ref('')
// 缓存计算结果,仅当依赖改变时重新计算
const fullName = computed(() => {
return `${first.value} ${last.value}`
})
// 也可设置 setter
const reversedName = computed({
get() {
return fullName.value.split('').reverse().join('')
},
set(value) {
const parts = value.split(' ')
first.value = parts[0] || ''
last.value = parts[1] || ''
}
})
</script>
<template>
<input v-model="first" placeholder="名字" />
<input v-model="last" placeholder="姓氏" />
<p>全名: {{ fullName }}</p>
<p>倒序: {{ reversedName }}</p>
</template>
✅ 优势:
- 基于依赖自动更新,性能高。
- 只有依赖变化时才重新计算。
- 支持双向绑定(通过
set)。
七、复杂业务逻辑封装:构建可复用的表单管理器
7.1 场景描述
我们想要构建一个通用的表单管理 Composable,支持:
- 表单数据绑定
- 验证规则配置
- 提交状态管理
- 错误提示展示
7.2 实现代码
// composables/useForm.js
import { ref, computed } from 'vue'
export function useForm(initialValues, validationRules = {}) {
const formData = ref({ ...initialValues })
const errors = ref({})
const isSubmitting = ref(false)
const isSuccess = ref(false)
// 验证函数
const validate = () => {
errors.value = {}
let isValid = true
Object.keys(validationRules).forEach((field) => {
const rule = validationRules[field]
const value = formData.value[field]
if (rule.required && !value) {
errors.value[field] = '此字段不能为空'
isValid = false
} else if (rule.pattern && !rule.pattern.test(value)) {
errors.value[field] = rule.message || '格式不正确'
isValid = false
}
})
return isValid
}
// 提交表单
const submit = async (onSubmit) => {
if (!validate()) return
isSubmitting.value = true
isSuccess.value = false
try {
await onSubmit(formData.value)
isSuccess.value = true
} catch (err) {
console.error('Submission failed:', err)
} finally {
isSubmitting.value = false
}
}
// 重置表单
const reset = () => {
formData.value = { ...initialValues }
errors.value = {}
isSuccess.value = false
}
// 获取字段值
const getField = (field) => formData.value[field]
// 设置字段值
const setField = (field, value) => {
formData.value[field] = value
// 清除该字段错误
if (errors.value[field]) {
delete errors.value[field]
}
}
return {
formData,
errors,
isSubmitting,
isSuccess,
validate,
submit,
reset,
getField,
setField
}
}
7.3 使用示例
<!-- LoginForm.vue -->
<script setup>
import { useForm } from '@/composables/useForm'
const formConfig = {
email: '',
password: ''
}
const validationRules = {
email: {
required: true,
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: '请输入有效的邮箱地址'
},
password: {
required: true,
min: 6
}
}
const { formData, errors, isSubmitting, submit, reset } = useForm(formConfig, validationRules)
const handleSubmit = async (data) => {
// 模拟提交
await new Promise(resolve => setTimeout(resolve, 1000))
console.log('Submitted:', data)
}
const handleLogin = () => {
submit(handleSubmit)
}
</script>
<template>
<form @submit.prevent="handleLogin">
<div>
<label>Email:</label>
<input
v-model="formData.email"
type="email"
:class="{ error: errors.email }"
/>
<span v-if="errors.email" class="error-msg">{{ errors.email }}</span>
</div>
<div>
<label>Password:</label>
<input
v-model="formData.password"
type="password"
:class="{ error: errors.password }"
/>
<span v-if="errors.password" class="error-msg">{{ errors.password }}</span>
</div>
<button type="submit" :disabled="isSubmitting">
{{ isSubmitting ? '登录中...' : '登录' }}
</button>
<button type="button" @click="reset">重置</button>
</form>
</template>
<style scoped>
.error {
border-color: red;
}
.error-msg {
color: red;
font-size: 12px;
}
</style>
✅ 优势总结:
- 逻辑高度抽象,可复用于注册、编辑、设置等多种表单。
- 支持动态规则配置。
- 易于扩展(如加入 debounce、auto-save 等)。
八、TypeScript 集成:提升开发体验与安全性
8.1 启用 TypeScript 支持
在 Vite 项目中,确保 tsconfig.json 已正确配置:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "Node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"]
},
"include": [
"src/**/*"
]
}
8.2 类型化 Composable
// composables/useUserStore.ts
import { Ref, ref, computed } from 'vue'
interface User {
id: number
name: string
email: string
}
export function useUserStore() {
const users = ref<User[]>([])
const addUser = (user: User) => {
users.value.push(user)
}
const findUserById = (id: number): User | undefined => {
return users.value.find(u => u.id === id)
}
const totalUsers = computed(() => users.value.length)
return {
users,
addUser,
findUserById,
totalUsers
}
}
8.3 与 defineProps 结合使用
// components/UserCard.vue
<script setup lang="ts">
import { defineProps } from 'vue'
interface Props {
user: {
id: number
name: string
avatar?: string
}
}
const props = defineProps<Props>()
</script>
✅ 好处:
- 编辑器智能提示
- 编译时类型检查
- 防止运行时错误
九、性能优化与最佳实践建议
9.1 避免不必要的响应式包裹
// ❌ 低效写法
const obj = reactive({ a: 1, b: 2 })
const x = ref(obj) // 多层嵌套导致性能下降
// ✅ 推荐写法
const obj = ref({ a: 1, b: 2 })
9.2 合理使用 computed 与 watch
- 用
computed处理纯函数式计算。 - 用
watch处理副作用(如网络请求、事件订阅)。 - 避免在
computed内部做异步操作。
9.3 使用 shallowRef 与 shallowReactive 优化大对象
对于大型数据结构,若不需深层响应,可用浅层版本:
const largeData = shallowRef({ /* huge object */ })
9.4 懒加载 Composable
// 只在需要时导入
if (condition) {
const { useAuth } = await import('@/composables/useAuth')
const auth = useAuth()
}
9.5 命名规范统一
- 所有 Composable 以
use开头 - 事件使用
camelCase - 避免使用
onXxx作为变量名
十、结语:迈向现代化 Vue 3 开发
Vue 3 Composition API 并不仅仅是一个语法升级,它是架构思维的一次跃迁。通过将逻辑按功能组织,我们能够构建出更清晰、更易维护、更可复用的组件系统。
无论你是个人开发者还是团队协作,掌握 Composition API 都意味着:
- 更好的代码组织
- 更强的类型支持
- 更高的开发效率
- 更少的重构成本
🌟 终极建议:从现在开始,所有新项目都使用
<script setup>+ Composition API + TypeScript,逐步迁移旧项目。
附录:常用 Composable 推荐清单
| 名称 | 功能 |
|---|---|
useLocalStorage |
持久化存储 |
useFetch |
HTTP 请求封装 |
useDebounce |
输入防抖 |
useClickOutside |
点击外部关闭弹窗 |
useIntersectionObserver |
懒加载/滚动检测 |
useWindowSize |
监听窗口大小变化 |
useAsyncState |
异步状态管理 |
📌 参考资料:
✅ 本文共约 6,500 字,涵盖从基础到高级的完整开发流程与最佳实践,适合中高级前端开发者深入学习与参考。

评论 (0)