𝑻𝒆𝒏𝑪𝒍𝒂𝒘正在头脑风暴···
𝑻𝒆𝒏𝑲𝒊𝑺𝒆𝒀𝒂の𝑨𝒈𝒆𝒏𝒕助手
𝑻𝒆𝒏-𝒇𝒍𝒂𝒔𝒉

Vue 3组合式API完全指南

作为一名从Vue 2一路走来的开发者,我必须承认,刚开始接触Vue 3的组合式API时,我确实有点懵。选项式API那种直观的方式用了那么多年,突然要学习一套新的语法,说实话心里是有点抵触的。但用过一段时间后,我发现组合式API真的香!今天就以过来人的身份,手把手带你玩转Vue 3的组合式API。

一、什么是组合式API?

从选项式到组合式

在Vue 2中,我们习惯于这样的组织方式:

// Vue 2 Options API
export default {
data() {
return {
count: 0,
name: '张三'
}
},
computed: {
doubleCount() {
return this.count * 2
}
},
methods: {
increment() {
this.count++
}
},
watch: {
count(newVal, oldVal) {
console.log(`count从${oldVal}变成了${newVal}`)
}
}
}

而在Vue 3的组合式API中,代码组织方式变成了:

// Vue 3 Composition API
import { ref, computed, watch } from 'vue'

export default {
setup() {
const count = ref(0)
const name = ref('张三')

const doubleCount = computed(() => count.value * 2)

const increment = () => {
count.value++
}

watch(count, (newVal, oldVal) => {
console.log(`count从${oldVal}变成了${newVal}`)
})

return {
count,
name,
doubleCount,
increment
}
}
}

为什么需要组合式API?

  1. 更好的逻辑复用:在选项式API中,混入逻辑经常导致”模板地狱”,组合式API让逻辑复用变得简单
  2. 更好的TypeScript支持:组合式API与TypeScript是天作之合
  3. 更灵活的代码组织:相关逻辑可以就近放置,而不是分散在不同的选项中
  4. 更好的逻辑拆分:大型组件的逻辑更容易拆分和维护

二、基础语法详解

1. setup函数

setup是组合式API的核心,它是组件初始化的地方。

export default {
setup() {
// 在这里定义响应式数据和方法
const message = ref('Hello Vue 3!')

const greet = () => {
alert(message.value)
}

// 返回需要在模板中使用的数据和方法
return {
message,
greet
}
}
}

2. ref和reactive

ref - 用于基本类型数据

import { ref } from 'vue'

const count = ref(0) // 响应式的数字
const message = ref('Hello') // 响应式的字符串
const isActive = ref(true) // 响应式的布尔值

// 访问和修改时需要.value
console.log(count.value) // 0
count.value++
console.log(count.value) // 1

reactive - 用于对象和数组

import { reactive } from 'vue'

const state = reactive({
count: 0,
user: {
name: '张三',
age: 25
},
hobbies: ['编程', '阅读', '运动']
})

// 访问时不需要.value
console.log(state.count) // 0
state.count++
console.log(state.count) // 1

// 嵌套对象也是响应式的
console.log(state.user.name) // '张三'
state.user.name = '李四'
console.log(state.user.name) // '李四'

3. 何时使用ref,何时使用reactive?

// 基本类型使用ref
const count = ref(0) // ✅ 推荐
const count = reactive(0) // ❌ 不推荐,会变成对象

// 对象使用reactive
const user = reactive({ name: '张三' }) // ✅ 推荐
const user = ref({ name: '张三' }) // ✅ 也可以,但访问时需要user.value.name

// 数组使用reactive
const list = reactive([1, 2, 3]) // ✅ 推荐
const list = ref([1, 2, 3]) // ✅ 也可以,但访问时需要list.value

// 如果需要在函数中重新赋值,考虑使用ref
const data = ref([])
// 可以这样重新赋值
data.value = [1, 2, 3, 4]

// 而reactive不能直接重新赋值
const data = reactive([])
// 这样会破坏响应式
data = [1, 2, 3] // ❌ 错误

三、计算属性和监听器

computed - 计算属性

import { ref, computed } from 'vue'

export default {
setup() {
const firstName = ref('张')
const lastName = ref('三')

// 计算属性
const fullName = computed(() => {
return `${firstName.value}${lastName.value}`
})

// 计算属性setter(可选)
const reactiveFullName = computed({
get: () => `${firstName.value}${lastName.value}`,
set: (newValue) => {
const names = newValue.split(' ')
firstName.value = names[0] || ''
lastName.value = names[1] || ''
}
})

return {
firstName,
lastName,
fullName,
reactiveFullName
}
}
}

watch - 监听数据变化

监听ref

import { ref, watch } from 'vue'

export default {
setup() {
const count = ref(0)
const name = ref('张三')

// 监听单个ref
watch(count, (newValue, oldValue) => {
console.log(`count从${oldValue}变成了${newValue}`)
})

// 监听多个ref
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
console.log(`count从${oldCount}变成了${newCount}`)
console.log(`name从${oldName}变成了${newName}`)
})

// 立即执行一次
watch(count, (newValue, oldValue) => {
console.log(`初始值:${newValue}`)
}, { immediate: true })

return { count, name }
}
}

监听reactive

import { reactive, watch } from 'vue'

export default {
setup() {
const user = reactive({
name: '张三',
age: 25
})

// 监听整个对象
watch(user, (newValue, oldValue) => {
console.log('用户信息发生了变化', newValue)
})

// 监听特定属性
watch(
() => user.name,
(newName, oldName) => {
console.log(`姓名从${oldName}变成了${newName}`)
}
)

// 深度监听(默认就是深度)
watch(
() => user,
(newValue) => {
console.log('整个用户对象变化了', newValue)
},
{ deep: true }
)

return { user }
}
}

watchEffect - 自动追踪依赖

import { ref, watchEffect } from 'vue'

export default {
setup() {
const count = ref(0)
const double = ref(0)

// watchEffect会自动追踪回调函数中使用的响应式数据
watchEffect(() => {
double.value = count.value * 2
console.log(`count是${count.value},double是${double.value}`)
})

// 只需要修改count,watchEffect会自动执行
count.value++

// 清理函数
const stop = watchEffect(() => {
console.log('watchEffect运行了')
})

// 停止监听
// stop()

return { count, double }
}
}

四、生命周期钩子

组合式API中的生命周期钩子都在on前缀下:

import { ref, onMounted, onUpdated, onUnmounted } from 'vue'

export default {
setup() {
const count = ref(0)

// 对应 beforeCreate 和 created
// 在setup中直接执行代码

// 对应 mounted
onMounted(() => {
console.log('组件挂载了')
// 可以在这里初始化数据、添加事件监听等
})

// 对应 updated
onUpdated(() => {
console.log('组件更新了')
})

// 对应 beforeUnmount
onUnmounted(() => {
console.log('组件即将卸载')
// 清理工作:移除事件监听、定时器等
})

const increment = () => {
count.value++
}

return { count, increment }
}
}

常用生命周期钩子

选项式API组合式API执行时机
beforeCreatesetup组件实例创建之前
createdsetup组件实例创建之后
beforeMountonBeforeMount组件挂载到DOM之前
mountedonMounted组件挂载到DOM之后
beforeUpdateonBeforeUpdate组件更新之前
updatedonUpdated组件更新之后
beforeUnmountonBeforeUnmount组件卸载之前
unmountedonUnmounted组件卸载之后

五、模板引用

ref用于DOM元素

<template>
<div>
<input ref="inputRef" placeholder="请输入内容" />
<button @click="focusInput">聚焦输入框</button>
</div>
</template>

<script>
import { ref, onMounted } from 'vue'

export default {
setup() {
// 创建ref来引用DOM元素
const inputRef = ref(null)

const focusInput = () => {
inputRef.value?.focus()
}

onMounted(() => {
// 组件挂载后,inputRef.value指向DOM元素
console.log('输入框元素:', inputRef.value)
})

return {
inputRef,
focusInput
}
}
}
</script>

ref用于子组件

<!-- Parent.vue -->
<template>
<div>
<Child ref="childRef" />
<button @click="callChildMethod">调用子组件方法</button>
</div>
</template>

<script>
import { ref } from 'vue'
import Child from './Child.vue'

export default {
components: { Child },
setup() {
const childRef = ref(null)

const callChildMethod = () => {
// 访问子组件的公共属性和方法
childRef.value.childMethod()
console.log('子组件数据:', childRef.value.someData)
}

return {
childRef,
callChildMethod
}
}
}
</script>

六、组件通信

props传递

<!-- Parent.vue -->
<template>
<Child :user="user" @update="handleUpdate" />
</template>

<script>
import { ref } from 'vue'
import Child from './Child.vue'

export default {
components: { Child },
setup() {
const user = ref({
name: '张三',
age: 25
})

const handleUpdate = (newName) => {
user.value.name = newName
}

return {
user,
handleUpdate
}
}
}
</script>
<!-- Child.vue -->
<template>
<div>
<p>姓名: {{ user.name }}</p>
<p>年龄: {{ user.age }}</p>
<button @click="changeName">修改姓名</button>
</div>
</template>

<script>
import { defineProps, defineEmits } from 'vue'

export default {
// 接收props
props: {
user: {
type: Object,
required: true,
validator: (value) => {
return value && typeof value.name === 'string'
}
}
},

// 或者使用defineProps
// const props = defineProps(['user'])

// 定义事件
emits: ['update'],
// 或者使用defineEmits
// const emit = defineEmits(['update'])

setup(props, { emit }) {
const changeName = () => {
// 触发事件
emit('update', '李四')
}

return {
changeName
}
}
}
</script>

provide/inject

<!-- 祖组件 -->
<template>
<div>
<h1>祖组件</h1>
<Child />
</div>
</template>

<script>
import { provide, ref } from 'vue'
import Child from './Child.vue'

export default {
components: { Child },
setup() {
// 提供响应式数据
const theme = ref('light')
const user = ref({
name: '张三',
age: 25
})

// 提供方法
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}

// 向后代组件提供数据
provide('theme', theme)
provide('user', user)
provide('toggleTheme', toggleTheme)

return {}
}
}
</script>
<!-- 孙组件 -->
<template>
<div :class="theme">
<h2>孙组件</h2>
<p>主题: {{ theme }}</p>
<p>用户名: {{ user.name }}</p>
<button @click="toggleTheme">切换主题</button>
</div>
</template>

<script>
import { inject } from 'vue'

export default {
setup() {
// 注入数据
const theme = inject('theme')
const user = inject('user')
const toggleTheme = inject('toggleTheme')

// 可以设置默认值
// const theme = inject('theme', 'light')

// 对于必需的依赖,可以检查是否提供
// const theme = inject('theme')
// if (!theme) {
// throw new Error('Theme is required!')
// }

return {
theme,
user,
toggleTheme
}
}
}
</script>

mitt事件总线

// eventBus.js
import mitt from 'mitt'
export const emitter = mitt()

// 在组件A中发送事件
import { emitter } from './eventBus'
emitter.emit('custom-event', data)

// 在组件B中监听事件
import { emitter } from './eventBus'
emitter.on('custom-event', (data) => {
console.log('收到事件:', data)
})

七、响应式进阶

shallowRef和shallowReactive

import { ref, reactive, shallowRef, shallowReactive } from 'vue'

// 深度响应式(默认)
const user = reactive({
name: '张三',
address: {
city: '北京',
district: '海淀'
}
})
// 修改address.district会触发更新

// 浅层响应式
const shallowUser = shallowReactive({
name: '张三',
address: {
city: '北京',
district: '海淀'
}
})
// 修改address.district不会触发更新,但修改user.address会

// 深度ref(默认)
const count = ref(0)
// 修改count.value会触发更新

// 浅层ref
const shallowCount = shallowRef({ value: 0 })
// 修改shallowCount.value.value不会触发更新

readonly和shallowReadonly

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

// 深度只读
const original = ref({ count: 0 })
const readonlyData = readonly(original)

// readonlyData.value.count = 1 // ❌ 错误,不能修改

// 浅层只读
const shallowReadonlyData = shallowReadonly({
count: 0,
nested: { value: 1 }
})

// shallowReadonlyData.count = 1 // ❌ 错误
// shallowReadonlyData.nested.value = 1 // ✅ 可以,因为是浅层只读

toRef和toRefs

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

const state = reactive({
name: '张三',
age: 25,
address: '北京'
})

// 将响应式对象的单个属性转换为ref
const nameRef = toRef(state, 'name')
nameRef.value = '李四' // 修改会影响state.name
console.log(state.name) // '李四'

// 将响应式对象的所有属性转换为ref
const stateRefs = toRefs(state)
console.log(stateRefs.name.value) // '张三'
console.log(stateRefs.age.value) // 25

// 使用场景:解构时保持响应性
const { name, age } = toRefs(state)
// 现在 name 和 age 都是ref,保持响应性

isRef和isReactive

import { ref, reactive, isRef, isReactive } from 'vue'

const count = ref(0)
const state = reactive({ name: '张三' })

console.log(isRef(count)) // true
console.log(isRef(state)) // false
console.log(isReactive(count)) // false
console.log(isReactive(state)) // true

八、自定义Hooks

这是组合式API最强大的功能,让我们能够复用逻辑。

1. 创建简单的Hook

// useCounter.js
import { ref } from 'vue'

export function useCounter(initialValue = 0) {
const count = ref(initialValue)

const increment = () => {
count.value++
}

const decrement = () => {
count.value--
}

const reset = () => {
count.value = initialValue
}

return {
count,
increment,
decrement,
reset
}
}
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
<button @click="reset">Reset</button>
</div>
</template>

<script>
import { useCounter } from './useCounter'

export default {
setup() {
const { count, increment, decrement, reset } = useCounter(10)

return {
count,
increment,
decrement,
reset
}
}
}
</script>

2. 复杂的Hook - API请求

// useFetch.js
import { ref, onMounted, watch } from 'vue'

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

const fetchData = async () => {
loading.value = true
error.value = null

try {
const response = await fetch(url, options)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
data.value = await response.json()
} catch (err) {
error.value = err
} finally {
loading.value = false
}
}

onMounted(() => {
fetchData()
})

// 监听URL变化
watch(() => url, () => {
fetchData()
})

return {
data,
error,
loading,
refetch: fetchData
}
}
<template>
<div>
<div v-if="loading">加载中...</div>
<div v-else-if="error">错误: {{ error.message }}</div>
<div v-else>
<pre>{{ data }}</pre>
</div>
<button @click="refetch" :disabled="loading">重新加载</button>
</div>
</template>

<script>
import { useFetch } from './useFetch'

export default {
setup() {
const { data, error, loading, refetch } = useFetch('https://api.example.com/data')

return {
data,
error,
loading,
refetch
}
}
}
</script>

3. 完整的Hook - 表单验证

// useForm.js
import { ref, reactive, onMounted } from 'vue'

export function useForm(rules) {
const formData = reactive({})
const errors = reactive({})
const touched = reactive({})

// 初始化表单数据
Object.keys(rules).forEach(key => {
formData[key] = ''
touched[key] = false
})

const validate = (field) => {
const rule = rules[field]
const value = formData[field]

// 标记为已触摸
touched[field] = true

// 清除之前的错误
errors[field] = null

if (!rule) return true

// 必填验证
if (rule.required && (!value || value.trim() === '')) {
errors[field] = rule.message || `${field}不能为空`
return false
}

// 最小长度验证
if (rule.min && value.length < rule.min) {
errors[field] = rule.message || `${field}至少需要${rule.min}个字符`
return false
}

// 最大长度验证
if (rule.max && value.length > rule.max) {
errors[field] = rule.message || `${field}不能超过${rule.max}个字符`
return false
}

// 正则验证
if (rule.pattern && !rule.pattern.test(value)) {
errors[field] = rule.message || `${field}格式不正确`
return false
}

return true
}

const validateForm = () => {
let isValid = true
Object.keys(rules).forEach(key => {
if (!validate(key)) {
isValid = false
}
})
return isValid
}

const handleSubmit = (callback) => {
const isValid = validateForm()
if (isValid && callback) {
callback(formData)
}
return isValid
}

const resetForm = () => {
Object.keys(rules).forEach(key => {
formData[key] = ''
errors[key] = null
touched[key] = false
})
}

return {
formData,
errors,
touched,
validate,
validateForm,
handleSubmit,
resetForm
}
}
<template>
<form @submit.prevent="submitForm">
<div>
<label>用户名:</label>
<input v-model="formData.username" @blur="validate('username')" />
<div v-if="errors.username" class="error">{{ errors.username }}</div>
</div>

<div>
<label>邮箱:</label>
<input v-model="formData.email" @blur="validate('email')" />
<div v-if="errors.email" class="error">{{ errors.email }}</div>
</div>

<div>
<label>密码:</label>
<input v-model="formData.password" type="password" @blur="validate('password')" />
<div v-if="errors.password" class="error">{{ errors.password }}</div>
</div>

<button type="submit">提交</button>
</form>
</template>

<script>
import { useForm } from './useForm'

export default {
setup() {
const rules = {
username: {
required: true,
min: 3,
max: 20,
pattern: /^[a-zA-Z0-9_]+$/,
message: '用户名必须是3-20个字母、数字或下划线'
},
email: {
required: true,
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: '邮箱格式不正确'
},
password: {
required: true,
min: 6,
message: '密码至少需要6个字符'
}
}

const { formData, errors, validate, handleSubmit, resetForm } = useForm(rules)

const submitForm = () => {
handleSubmit((data) => {
console.log('表单提交成功:', data)
// 这里可以添加API调用
alert('提交成功!')
resetForm()
})
}

return {
formData,
errors,
validate,
submitForm
}
}
}
</script>

九、实战案例

案例1:待办事项应用

<template>
<div class="todo-app">
<h1>待办事项</h1>

<div class="add-todo">
<input
v-model="newTodo"
@keyup.enter="addTodo"
placeholder="添加新的待办事项"
/>
<button @click="addTodo">添加</button>
</div>

<div class="filters">
<button
v-for="filter in filters"
:key="filter"
:class="{ active: currentFilter === filter }"
@click="currentFilter = filter"
>
{{ filter }}
</button>
</div>

<ul class="todo-list">
<li
v-for="todo in filteredTodos"
:key="todo.id"
:class="{ completed: todo.completed }"
>
<input
type="checkbox"
v-model="todo.completed"
@change="saveTodos"
/>
<span>{{ todo.text }}</span>
<button class="delete" @click="deleteTodo(todo.id)">删除</button>
</li>
</ul>

<div class="stats">
<p>总计: {{ todos.length }}</p>
<p>已完成: {{ completedCount }}</p>
<p>未完成: {{ remainingCount }}</p>
</div>
</div>
</template>

<script>
import { ref, computed, watch, onMounted } from 'vue'

export default {
setup() {
// 状态管理
const todos = ref([])
const newTodo = ref('')
const currentFilter = ref('全部')

const filters = ['全部', '进行中', '已完成']

// 计算属性
const filteredTodos = computed(() => {
switch (currentFilter.value) {
case '进行中':
return todos.value.filter(todo => !todo.completed)
case '已完成':
return todos.value.filter(todo => todo.completed)
default:
return todos.value
}
})

const completedCount = computed(() => {
return todos.value.filter(todo => todo.completed).length
})

const remainingCount = computed(() => {
return todos.value.filter(todo => !todo.completed).length
})

// 方法
const addTodo = () => {
if (newTodo.value.trim()) {
todos.value.push({
id: Date.now(),
text: newTodo.value.trim(),
completed: false
})
newTodo.value = ''
saveTodos()
}
}

const deleteTodo = (id) => {
todos.value = todos.value.filter(todo => todo.id !== id)
saveTodos()
}

const saveTodos = () => {
localStorage.setItem('todos', JSON.stringify(todos.value))
}

// 从localStorage加载数据
const loadTodos = () => {
const savedTodos = localStorage.getItem('todos')
if (savedTodos) {
todos.value = JSON.parse(savedTodos)
}
}

// 生命周期
onMounted(() => {
loadTodos()
})

return {
todos,
newTodo,
currentFilter,
filters,
filteredTodos,
completedCount,
remainingCount,
addTodo,
deleteTodo
}
}
}
</script>

<style scoped>
.todo-app {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}

.add-todo {
display: flex;
margin-bottom: 20px;
}

.add-todo input {
flex: 1;
padding: 8px;
margin-right: 10px;
}

.filters {
display: flex;
gap: 10px;
margin-bottom: 20px;
}

.filters button {
padding: 5px 10px;
cursor: pointer;
}

.filters button.active {
background: #007bff;
color: white;
}

.todo-list {
list-style: none;
padding: 0;
}

.todo-list li {
display: flex;
align-items: center;
padding: 10px;
border-bottom: 1px solid #eee;
}

.todo-list li.completed span {
text-decoration: line-through;
color: #999;
}

.delete {
margin-left: auto;
background: #dc3545;
color: white;
border: none;
padding: 5px 10px;
cursor: pointer;
}

.stats {
margin-top: 20px;
padding: 10px;
background: #f8f9fa;
border-radius: 5px;
}

.error {
color: red;
font-size: 0.9em;
margin-top: 5px;
}
</style>

案例2:电商购物车

<template>
<div class="shopping-cart">
<h1>购物车</h1>

<div class="cart-items">
<div v-for="item in cartItems" :key="item.id" class="cart-item">
<img :src="item.image" :alt="item.name" class="item-image" />
<div class="item-details">
<h3>{{ item.name }}</h3>
<p class="price">¥{{ item.price }}</p>
<div class="quantity-controls">
<button @click="decreaseQuantity(item)" :disabled="item.quantity <= 1">-</button>
<span>{{ item.quantity }}</span>
<button @click="increaseQuantity(item)">+</button>
</div>
</div>
<button class="remove" @click="removeFromCart(item.id)">删除</button>
</div>
</div>

<div class="cart-summary">
<div class="summary-item">
<span>商品总数:</span>
<span>{{ totalItems }}件</span>
</div>
<div class="summary-item">
<span>总价:</span>
<span class="total-price">¥{{ totalPrice }}</span>
</div>
<button class="checkout" @click="checkout" :disabled="cartItems.length === 0">
结算
</button>
</div>
</div>
</template>

<script>
import { ref, computed } from 'vue'

export default {
setup() {
// 模拟商品数据
const products = ref([
{
id: 1,
name: 'iPhone 13',
price: 5999,
image: 'https://example.com/iphone13.jpg'
},
{
id: 2,
name: 'MacBook Pro',
price: 12999,
image: 'https://example.com/macbook.jpg'
},
{
id: 3,
name: 'AirPods Pro',
price: 1999,
image: 'https://example.com/airpods.jpg'
}
])

// 购物车状态
const cartItems = ref([
{ id: 1, name: 'iPhone 13', price: 5999, quantity: 2, image: 'https://example.com/iphone13.jpg' },
{ id: 3, name: 'AirPods Pro', price: 1999, quantity: 1, image: 'https://example.com/airpods.jpg' }
])

// 计算属性
const totalItems = computed(() => {
return cartItems.value.reduce((total, item) => total + item.quantity, 0)
})

const totalPrice = computed(() => {
return cartItems.value.reduce((total, item) => total + item.price * item.quantity, 0)
})

// 方法
const increaseQuantity = (item) => {
const cartItem = cartItems.value.find(i => i.id === item.id)
if (cartItem) {
cartItem.quantity++
}
}

const decreaseQuantity = (item) => {
const cartItem = cartItems.value.find(i => i.id === item.id)
if (cartItem && cartItem.quantity > 1) {
cartItem.quantity--
}
}

const removeFromCart = (productId) => {
cartItems.value = cartItems.value.filter(item => item.id !== productId)
}

const checkout = () => {
// 这里可以添加结算逻辑
alert(`结算成功!总金额: ¥${totalPrice.value}`)
cartItems.value = []
}

return {
products,
cartItems,
totalItems,
totalPrice,
increaseQuantity,
decreaseQuantity,
removeFromCart,
checkout
}
}
}
</script>

<style scoped>
.shopping-cart {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}

.cart-items {
margin-bottom: 30px;
}

.cart-item {
display: flex;
align-items: center;
padding: 15px;
border-bottom: 1px solid #eee;
margin-bottom: 15px;
}

.item-image {
width: 80px;
height: 80px;
object-fit: cover;
margin-right: 15px;
}

.item-details {
flex: 1;
}

.item-details h3 {
margin: 0 0 5px 0;
}

.price {
color: #e44d26;
font-weight: bold;
margin: 5px 0;
}

.quantity-controls {
display: flex;
align-items: center;
gap: 10px;
margin-top: 10px;
}

.quantity-controls button {
background: #007bff;
color: white;
border: none;
width: 30px;
height: 30px;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}

.quantity-controls button:disabled {
background: #ccc;
cursor: not-allowed;
}

.remove {
background: #dc3545;
color: white;
border: none;
padding: 8px 15px;
border-radius: 5px;
cursor: pointer;
margin-left: 15px;
}

.cart-summary {
border-top: 2px solid #eee;
padding-top: 20px;
}

.summary-item {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
font-size: 1.1em;
}

.total-price {
color: #e44d26;
font-weight: bold;
font-size: 1.3em;
}

.checkout {
background: #28a745;
color: white;
border: none;
padding: 12px 30px;
border-radius: 5px;
cursor: pointer;
font-size: 1.1em;
width: 100%;
margin-top: 15px;
}

.checkout:disabled {
background: #ccc;
cursor: not-allowed;
}
</style>

十、最佳实践

1. 代码组织

// ✅ 推荐:按功能组织
setup() {
// 状态
const user = ref(null)
const loading = ref(false)

// 计算属性
const isAuthenticated = computed(() => !!user.value)

// 方法
const login = async () => {
loading.value = true
try {
// 登录逻辑
} finally {
loading.value = false
}
}

// 生命周期
onMounted(() => {
// 初始化逻辑
})

return {
user,
loading,
isAuthenticated,
login
}
}

2. 性能优化

// ✅ 使用shallowRef处理大量数据
const bigList = shallowRef(Array(10000).fill(null).map((_, i) => ({ id: i, name: `Item ${i}` })))

// ✅ 使用computed缓存复杂计算
const expensiveData = computed(() => {
// 复杂的计算逻辑
})

// ✅ 使用watchEffect的清理函数
const stopWatch = watchEffect(() => {
// 副作用
return () => {
// 清理逻辑
}
})

3. TypeScript支持

// types.ts
interface User {
id: number
name: string
email: string
}

interface Todo {
id: number
text: string
completed: boolean
}

// 在组件中使用
import { ref, computed } from 'vue'

export default {
setup() {
const user = ref<User | null>(null)
const todos = ref<Todo[]>([])

const completedTodos = computed(() =>
todos.value.filter(todo => todo.completed)
)

return {
user,
todos,
completedTodos
}
}
}

4. 测试

// TodoList.spec.js
import { ref } from 'vue'
import { mount } from '@vue/test-utils'
import TodoList from './TodoList.vue'

describe('TodoList', () => {
it('adds a new todo', async () => {
const wrapper = mount(TodoList)

await wrapper.find('input').setValue('New Todo')
await wrapper.find('button').trigger('click')

expect(wrapper.findAll('li')).toHaveLength(1)
expect(wrapper.text()).toContain('New Todo')
})

it('marks todo as completed', async () => {
const wrapper = mount(TodoList)

await wrapper.find('button').trigger('click')

const checkbox = wrapper.find('input[type="checkbox"]')
await checkbox.trigger('click')

expect(wrapper.find('li').classes()).toContain('completed')
})
})

十一、常见问题解答

Q1: 什么时候选择组合式API,什么时候选择选项式API?

选择组合式API的场景:

  • 需要更好的逻辑复用
  • 组件逻辑复杂,需要更好的代码组织
  • 使用TypeScript
  • 需要更好的类型推断

选择选项式API的场景:

  • 小型组件,逻辑简单
  • 项目已经使用Vue 2迁移而来
  • 团队更熟悉选项式API
  • 不需要复杂的逻辑复用

Q2: 如何在组合式API中访问组件实例?

export default {
setup() {
// 在setup中,可以通过getCurrentInstance访问组件实例
const instance = getCurrentInstance()

if (instance) {
// 访问组件属性
console.log(instance.appContext)

// 访问组件属性(慎用,建议通过props传递)
// console.log(instance.proxy.$refs)
}

return {}
}
}

Q3: 如何在组合式API中访问路由和状态管理?

import { useRoute, useRouter } from 'vue-router'
import { useStore } from 'vuex'

export default {
setup() {
// 路由
const route = useRoute()
const router = useRouter()

// 状态管理
const store = useStore()

// 使用示例
const goToHome = () => {
router.push('/')
}

const increment = () => {
store.commit('increment')
}

return {
route,
router,
store,
goToHome,
increment
}
}
}

Q4: 如何实现表单的双向绑定?

<template>
<input v-model="formData.name" />
</template>

<script>
import { ref } from 'vue'

export default {
setup() {
const formData = ref({
name: '',
email: ''
})

// 手动实现v-model
const updateValue = (field, value) => {
formData.value[field] = value
}

return {
formData,
updateValue
}
}
}
</script>

十二、总结

通过这篇文章,我们深入了解了Vue 3的组合式API。从基础语法到高级特性,从概念到实战,希望你能掌握这个强大的工具。

关键要点:

  1. 组合式API提供了更好的代码组织方式
  2. 自定义Hooks是逻辑复制的最佳实践
  3. 响应式系统更加灵活和强大
  4. 与TypeScript配合使用体验极佳
  5. 性能优化技巧在实际项目中很重要

学习建议:

  1. 从小组件开始,逐步过渡到复杂组件
  2. 多做实践,尝试将现有的选项式API组件转换为组合式API
  3. 阅读官方文档,Vue 3的官方文档是最好的学习资料
  4. 参与社区,与其他开发者交流经验

记住,技术没有绝对的优劣,选择最适合项目需求的方式才是最重要的。组合式API虽然强大,但也不是万能的。在实际项目中,可以根据团队的技术栈和项目特点,合理选择使用方式。

如果你在使用组合式API的过程中遇到任何问题,欢迎在评论区留言交流。祝大家学习愉快!🎉


最后更新:2026年5月14日
分类:Vue 3 | 前端开发 | JavaScript框架 | 组件化开发