引言
Vue 3 的发布带来了革命性的变化,其中最引人注目的就是 Composition API 的引入。这一新特性为开发者提供了更加灵活和强大的组件开发方式,特别是在处理复杂组件逻辑时表现出色。本文将深入探讨 Vue 3 Composition API 的核心特性,包括 setup 函数、响应式 API 以及组合式逻辑复用等,并通过实际项目案例展示如何构建高效、可维护的现代化前端应用。
Vue 3 Composition API 核心概念
什么是 Composition API?
Composition API 是 Vue 3 中引入的一种新的组件开发方式,它允许开发者以函数的形式组织和复用组件逻辑。与传统的 Options API 相比,Composition API 更加灵活,能够更好地处理复杂的组件逻辑,特别是在需要在多个组件间共享逻辑时表现出色。
Setup 函数详解
setup 是 Composition API 的入口函数,它在组件实例创建之前执行,用于定义响应式数据、计算属性、方法等。setup 函数接收两个参数:props 和 context。
import { ref, reactive } from 'vue'
export default {
props: {
title: String
},
setup(props, context) {
// 在这里定义响应式数据和逻辑
const count = ref(0)
const user = reactive({
name: 'John',
age: 30
})
return {
count,
user
}
}
}
响应式 API 深度解析
Ref API 的使用
ref 是最基础的响应式 API,用于创建一个响应式的引用。它可以包装任何类型的值,并提供 .value 属性来访问和修改值。
import { ref } from 'vue'
// 创建基本类型响应式数据
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
// 创建对象响应式数据
const obj = ref({ name: 'Vue', version: '3.0' })
console.log(obj.value.name) // Vue
obj.value.name = 'React'
console.log(obj.value.name) // React
Reactive API 的应用
reactive 用于创建响应式的对象,与 ref 不同的是,它直接返回一个响应式对象,不需要通过 .value 访问。
import { reactive } from 'vue'
const state = reactive({
count: 0,
user: {
name: 'John',
age: 30
}
})
// 修改数据会自动触发更新
state.count = 1
state.user.name = 'Jane'
Computed 计算属性
computed 用于创建计算属性,它会根据依赖的响应式数据自动计算并缓存结果。
import { ref, computed } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
// 基础用法
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`
})
// 带有 getter 和 setter 的计算属性
const reversedName = computed({
get: () => {
return firstName.value.split('').reverse().join('')
},
set: (value) => {
const names = value.split(' ')
firstName.value = names[0]
lastName.value = names[1]
}
})
组件状态管理实战
简单计数器组件示例
让我们从一个简单的计数器组件开始,展示如何使用 Composition API 管理组件状态:
<template>
<div class="counter">
<h2>{{ title }}</h2>
<p>当前计数: {{ count }}</p>
<button @click="increment">增加</button>
<button @click="decrement">减少</button>
<button @click="reset">重置</button>
</div>
</template>
<script>
import { ref, computed } from 'vue'
export default {
name: 'Counter',
props: {
title: {
type: String,
default: '计数器'
}
},
setup(props) {
// 响应式状态
const count = ref(0)
// 计算属性
const isPositive = computed(() => count.value >= 0)
// 方法
const increment = () => {
count.value++
}
const decrement = () => {
count.value--
}
const reset = () => {
count.value = 0
}
// 返回给模板使用的数据和方法
return {
count,
isPositive,
increment,
decrement,
reset
}
}
}
</script>
<style scoped>
.counter {
padding: 20px;
border: 1px solid #ccc;
border-radius: 4px;
margin: 10px;
}
button {
margin: 5px;
padding: 8px 16px;
cursor: pointer;
}
</style>
复杂表单组件状态管理
在实际项目中,我们经常需要处理复杂的表单逻辑。下面是一个购物车商品编辑组件的示例:
<template>
<div class="product-form">
<h3>{{ title }}</h3>
<!-- 基本信息 -->
<div class="form-group">
<label>商品名称:</label>
<input v-model="formData.name" type="text" />
</div>
<div class="form-group">
<label>价格:</label>
<input v-model.number="formData.price" type="number" />
</div>
<div class="form-group">
<label>库存数量:</label>
<input v-model.number="formData.stock" type="number" />
</div>
<!-- 分类选择 -->
<div class="form-group">
<label>商品分类:</label>
<select v-model="formData.category">
<option value="">请选择分类</option>
<option value="electronics">电子产品</option>
<option value="clothing">服装</option>
<option value="books">图书</option>
</select>
</div>
<!-- 标签选择 -->
<div class="form-group">
<label>标签:</label>
<div class="tag-container">
<span
v-for="tag in availableTags"
:key="tag"
:class="{ active: formData.tags.includes(tag) }"
@click="toggleTag(tag)"
class="tag"
>
{{ tag }}
</span>
</div>
</div>
<!-- 保存按钮 -->
<button @click="save" :disabled="!isFormValid">保存</button>
<button @click="reset">重置</button>
<!-- 表单验证信息 -->
<div v-if="validationErrors.length" class="errors">
<ul>
<li v-for="error in validationErrors" :key="error">{{ error }}</li>
</ul>
</div>
</div>
</template>
<script>
import { ref, reactive, computed } from 'vue'
export default {
name: 'ProductForm',
props: {
title: {
type: String,
default: '商品编辑'
},
initialData: {
type: Object,
default: () => ({})
}
},
setup(props) {
// 表单数据
const formData = reactive({
name: props.initialData.name || '',
price: props.initialData.price || 0,
stock: props.initialData.stock || 0,
category: props.initialData.category || '',
tags: props.initialData.tags || []
})
// 可用标签
const availableTags = ['热销', '新品', '推荐', '特价']
// 验证错误
const validationErrors = ref([])
// 表单验证
const isFormValid = computed(() => {
const errors = []
if (!formData.name.trim()) {
errors.push('商品名称不能为空')
}
if (formData.price <= 0) {
errors.push('价格必须大于0')
}
if (formData.stock < 0) {
errors.push('库存数量不能为负数')
}
validationErrors.value = errors
return errors.length === 0
})
// 切换标签
const toggleTag = (tag) => {
const index = formData.tags.indexOf(tag)
if (index > -1) {
formData.tags.splice(index, 1)
} else {
formData.tags.push(tag)
}
}
// 保存表单
const save = () => {
if (isFormValid.value) {
console.log('保存数据:', formData)
// 这里可以调用 API 保存数据
alert('数据保存成功!')
}
}
// 重置表单
const reset = () => {
Object.assign(formData, props.initialData)
validationErrors.value = []
}
return {
formData,
availableTags,
validationErrors,
isFormValid,
toggleTag,
save,
reset
}
}
}
</script>
<style scoped>
.product-form {
padding: 20px;
border: 1px solid #ddd;
border-radius: 4px;
margin: 10px 0;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input,
.form-group select {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
}
.tag-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 5px;
}
.tag {
padding: 4px 8px;
background-color: #f0f0f0;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
}
.tag:hover {
background-color: #e0e0e0;
}
.tag.active {
background-color: #007bff;
color: white;
}
.errors {
margin-top: 10px;
padding: 10px;
background-color: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 4px;
color: #721c24;
}
button {
margin-right: 10px;
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
组合式逻辑复用
自定义 Composable 函数
Composition API 的最大优势之一是能够将可复用的逻辑封装成自定义的 Composable 函数。让我们创建一个用户数据管理的 Composable:
// composables/useUser.js
import { ref, reactive, computed } from 'vue'
export function useUser() {
// 响应式状态
const users = ref([])
const loading = ref(false)
const error = ref(null)
// 当前用户
const currentUser = reactive({
id: null,
name: '',
email: ''
})
// 计算属性
const userCount = computed(() => users.value.length)
// 方法
const fetchUsers = async () => {
loading.value = true
error.value = null
try {
// 模拟 API 调用
const response = await new Promise(resolve => {
setTimeout(() => {
resolve({
data: [
{ id: 1, name: 'John', email: 'john@example.com' },
{ id: 2, name: 'Jane', email: 'jane@example.com' }
]
})
}, 1000)
})
users.value = response.data
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
const addUser = (userData) => {
const newUser = {
id: Date.now(),
...userData
}
users.value.push(newUser)
}
const updateUser = (id, userData) => {
const index = users.value.findIndex(user => user.id === id)
if (index > -1) {
Object.assign(users.value[index], userData)
}
}
const deleteUser = (id) => {
users.value = users.value.filter(user => user.id !== id)
}
// 初始化
fetchUsers()
return {
users,
loading,
error,
currentUser,
userCount,
fetchUsers,
addUser,
updateUser,
deleteUser
}
}
使用自定义 Composable
现在让我们在组件中使用这个自定义的 Composable:
<template>
<div class="user-management">
<h2>用户管理</h2>
<!-- 加载状态 -->
<div v-if="loading">加载中...</div>
<!-- 错误信息 -->
<div v-else-if="error" class="error">{{ error }}</div>
<!-- 用户列表 -->
<div v-else>
<p>总用户数: {{ userCount }}</p>
<div class="user-list">
<div
v-for="user in users"
:key="user.id"
class="user-item"
>
<span>{{ user.name }} - {{ user.email }}</span>
<button @click="deleteUser(user.id)">删除</button>
</div>
</div>
<!-- 添加用户表单 -->
<form @submit.prevent="handleSubmit" class="add-user-form">
<input
v-model="newUser.name"
placeholder="用户名"
required
/>
<input
v-model="newUser.email"
placeholder="邮箱"
type="email"
required
/>
<button type="submit">添加用户</button>
</form>
</div>
</div>
</template>
<script>
import { ref } from 'vue'
import { useUser } from '@/composables/useUser'
export default {
name: 'UserManagement',
setup() {
// 使用自定义 Composable
const {
users,
loading,
error,
userCount,
addUser,
deleteUser
} = useUser()
// 新用户数据
const newUser = ref({
name: '',
email: ''
})
// 处理表单提交
const handleSubmit = () => {
if (newUser.value.name && newUser.value.email) {
addUser({
name: newUser.value.name,
email: newUser.value.email
})
// 重置表单
newUser.value = { name: '', email: '' }
}
}
return {
users,
loading,
error,
userCount,
newUser,
handleSubmit,
deleteUser
}
}
}
</script>
<style scoped>
.user-management {
padding: 20px;
}
.user-list {
margin: 20px 0;
}
.user-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
border: 1px solid #eee;
margin-bottom: 5px;
border-radius: 4px;
}
.add-user-form {
display: flex;
gap: 10px;
margin-top: 20px;
}
.add-user-form input {
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
}
.add-user-form button {
padding: 8px 16px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.error {
color: red;
padding: 10px;
background-color: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 4px;
}
</style>
高级响应式编程技巧
响应式数据的深度监听
在 Vue 3 中,reactive 创建的对象是深层响应式的,这意味着对象内部的所有嵌套属性都会被监听:
import { reactive, watch } from 'vue'
const state = reactive({
user: {
profile: {
name: 'John',
age: 30
}
},
settings: {
theme: 'light',
language: 'zh-CN'
}
})
// 监听深层变化
watch(() => state.user.profile.name, (newVal, oldVal) => {
console.log(`姓名从 ${oldVal} 变为 ${newVal}`)
})
// 修改嵌套属性会触发监听器
state.user.profile.name = 'Jane' // 输出: 姓名从 John 变为 Jane
使用 watchEffect 进行自动依赖追踪
watchEffect 会自动追踪其内部函数执行时使用的响应式数据,无需显式指定依赖:
import { ref, watchEffect } from 'vue'
const count = ref(0)
const doubleCount = ref(0)
// 自动追踪依赖
watchEffect(() => {
doubleCount.value = count.value * 2
console.log(`当前计数: ${count.value}, 双倍计数: ${doubleCount.value}`)
})
// 当 count 改变时,watchEffect 会自动重新执行
count.value++ // 输出: 当前计数: 1, 双倍计数: 2
条件性响应式数据
在某些场景下,我们可能需要根据条件创建响应式数据:
import { ref, computed } from 'vue'
export default {
setup() {
const showDetails = ref(false)
// 根据条件创建响应式数据
const userDetails = computed(() => {
if (showDetails.value) {
return {
name: 'John',
email: 'john@example.com',
phone: '123-456-7890'
}
}
return {
name: 'John',
email: 'john@example.com'
}
})
// 可以在模板中使用
return {
showDetails,
userDetails
}
}
}
性能优化最佳实践
合理使用响应式 API
// ❌ 不推荐:不必要的响应式包装
const data = ref({
items: ['item1', 'item2', 'item3']
})
// ✅ 推荐:只对需要响应式变化的数据进行包装
const items = ref(['item1', 'item2', 'item3'])
// 对于不需要响应式变化的纯数据,可以使用普通对象
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000
}
避免过度监听
import { ref, watch } from 'vue'
export default {
setup() {
const count = ref(0)
const name = ref('')
// ❌ 不推荐:监听不需要的响应式数据
watch([count, name], () => {
console.log('任何变化都会触发')
})
// ✅ 推荐:只监听需要的数据
watch(count, (newVal) => {
console.log(`计数变化: ${newVal}`)
})
return { count, name }
}
}
使用 provide/inject 进行跨层级通信
// 父组件
import { provide, ref } from 'vue'
export default {
setup() {
const theme = ref('light')
const user = ref({ name: 'John' })
provide('theme', theme)
provide('user', user)
return { theme, user }
}
}
// 子组件
import { inject } from 'vue'
export default {
setup() {
const theme = inject('theme')
const user = inject('user')
return { theme, user }
}
}
实际项目应用案例
电商商品列表组件
让我们构建一个完整的电商商品列表组件,展示如何在实际项目中应用 Composition API:
<template>
<div class="product-list">
<!-- 搜索和筛选 -->
<div class="controls">
<input
v-model="searchQuery"
placeholder="搜索商品..."
class="search-input"
/>
<select v-model="selectedCategory" class="category-select">
<option value="">所有分类</option>
<option value="electronics">电子产品</option>
<option value="clothing">服装</option>
<option value="books">图书</option>
</select>
<button @click="clearFilters">清除筛选</button>
</div>
<!-- 商品列表 -->
<div class="products-grid">
<div
v-for="product in filteredProducts"
:key="product.id"
class="product-card"
>
<img :src="product.image" :alt="product.name" />
<h3>{{ product.name }}</h3>
<p class="price">¥{{ product.price }}</p>
<p class="category">{{ product.category }}</p>
<button @click="addToCart(product)" class="add-to-cart">
加入购物车
</button>
</div>
</div>
<!-- 分页 -->
<div class="pagination">
<button
:disabled="currentPage === 1"
@click="goToPage(currentPage - 1)"
>
上一页
</button>
<span>第 {{ currentPage }} 页,共 {{ totalPages }} 页</span>
<button
:disabled="currentPage === totalPages"
@click="goToPage(currentPage + 1)"
>
下一页
</button>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading">加载中...</div>
<!-- 错误信息 -->
<div v-if="error" class="error">{{ error }}</div>
</div>
</template>
<script>
import { ref, reactive, computed, watch } from 'vue'
import { useCart } from '@/composables/useCart'
export default {
name: 'ProductList',
setup() {
// 响应式状态
const products = ref([])
const loading = ref(false)
const error = ref(null)
const searchQuery = ref('')
const selectedCategory = ref('')
// 分页状态
const currentPage = ref(1)
const pageSize = ref(12)
// 购物车相关
const { addToCart } = useCart()
// 计算属性
const filteredProducts = computed(() => {
let result = products.value
// 搜索过滤
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
result = result.filter(product =>
product.name.toLowerCase().includes(query) ||
product.description.toLowerCase().includes(query)
)
}
// 分类过滤
if (selectedCategory.value) {
result = result.filter(product =>
product.category === selectedCategory.value
)
}
return result
})
const totalPages = computed(() => {
return Math.ceil(filteredProducts.value.length / pageSize.value)
})
const paginatedProducts = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return filteredProducts.value.slice(start, end)
})
// 方法
const fetchProducts = async () => {
loading.value = true
error.value = null
try {
// 模拟 API 调用
const response = await new Promise(resolve => {
setTimeout(() => {
resolve({
data: [
{ id: 1, name: 'iPhone 13', price: 6999, category: 'electronics', image: '/images/iphone.jpg' },
{ id: 2, name: 'MacBook Pro', price: 12999, category: 'electronics', image: '/images/macbook.jpg' },
{ id: 3, name: 'T-Shirt', price: 99, category: 'clothing', image: '/images/tshirt.jpg' },
{ id: 4, name: 'JavaScript高级程序设计', price: 89, category: 'books', image: '/images/js-book.jpg' }
]
})
}, 1000)
})
products.value = response.data
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
const clearFilters = () => {
searchQuery.value = ''
selectedCategory.value = ''
currentPage.value = 1
}
const goToPage = (page) => {
if (page >= 1 && page <= totalPages.value) {
currentPage.value = page
}
}
// 监听筛选条件变化
watch([searchQuery, selectedCategory], () => {
currentPage.value = 1 // 重置到第一页
})
// 初始化数据
fetchProducts()
return {
products,
loading,
error,
searchQuery,
selectedCategory,
currentPage,
totalPages,
paginatedProducts,
addToCart,
clearFilters,
goToPage
}
}
}
</script>
<style scoped>
.product-list {
padding: 20px;
}
.controls {
display: flex;
gap: 10px;
margin-bottom: 20px;
align-items: center;
flex-wrap: wrap;
}
.search-input,
.category-select {
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
}
.products-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.product-card {
border: 1px solid #eee;
border-radius: 8px;
padding: 15px;
text-align: center;
transition: transform 0.2s;
}
.product-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.product-card img {
width: 100%;
height: 200px;
object-fit: cover;
border-radius: 4px;
margin-bottom: 10px;
}
.price {
font-size
评论 (0)