引言
Vue 3的发布带来了全新的Composition API,这一创新性的API设计彻底改变了我们构建Vue应用的方式。相比于Vue 2的选项式API,Composition API提供了更灵活、更强大的组件逻辑组织方式,特别是在处理复杂组件和状态管理方面展现出了卓越的能力。
本文将深入探讨Vue 3 Composition API的核心概念,详细解析setup函数的使用、响应式API的高级特性,以及如何通过组合式逻辑复用来实现组件的高效复用。通过构建实际的复杂组件和状态管理方案,我们将展示Composition API在现代前端开发中的强大能力。
Vue 3 Composition API核心概念
什么是Composition API
Composition API是Vue 3引入的一种新的组件逻辑组织方式。它允许我们将组件的逻辑按照功能进行分组,而不是按照选项类型进行组织。这种设计模式使得代码更加灵活,便于维护和复用。
与Vue 2的选项式API相比,Composition API的主要优势包括:
- 更好的逻辑复用:通过组合函数实现逻辑复用,避免了混入(mixins)的命名冲突问题
- 更灵活的代码组织:可以按照功能逻辑而非选项类型来组织代码
- 更强的类型支持:在TypeScript环境中提供更好的类型推断
- 更清晰的代码结构:逻辑更加集中,便于理解和维护
setup函数详解
setup函数是Composition API的核心入口。它在组件实例创建之前执行,接收props和context作为参数。
// 基本setup函数用法
export default {
setup(props, context) {
// 组件逻辑
return {
// 需要暴露给模板的属性和方法
}
}
}
setup函数的返回值可以是一个对象,该对象的属性会被暴露给模板使用。同时,setup函数内部可以访问组件的props和上下文信息。
import { ref, reactive } from 'vue'
export default {
props: {
title: String,
count: Number
},
setup(props, context) {
// 访问props
console.log(props.title)
// 使用响应式数据
const counter = ref(0)
const state = reactive({
name: 'Vue',
version: '3.0'
})
// 定义方法
const increment = () => {
counter.value++
}
// 返回给模板使用的数据和方法
return {
counter,
state,
increment
}
}
}
响式API高级特性
ref与reactive的区别与使用
在Composition API中,ref和reactive是两个核心的响应式API,它们各有不同的使用场景。
import { ref, reactive, toRefs, toRaw } from 'vue'
// ref用于基本数据类型
const count = ref(0)
const message = ref('Hello')
// reactive用于对象类型
const state = reactive({
user: {
name: 'John',
age: 30
},
items: []
})
// 访问ref值需要使用.value
console.log(count.value) // 0
count.value = 10
// 访问reactive对象的属性无需.value
console.log(state.user.name) // John
响应式数据的深入理解
import { ref, reactive, watch, watchEffect } from 'vue'
export default {
setup() {
const count = ref(0)
const user = reactive({
name: 'Alice',
age: 25
})
// watch监听单个响应式数据
watch(count, (newVal, oldVal) => {
console.log(`count changed from ${oldVal} to ${newVal}`)
})
// watch监听对象属性
watch(() => user.name, (newVal, oldVal) => {
console.log(`name changed from ${oldVal} to ${newVal}`)
})
// watchEffect自动追踪依赖
watchEffect(() => {
console.log(`User: ${user.name}, Age: ${user.age}`)
})
// 深度监听
const deepState = reactive({
nested: {
data: {
value: 1
}
}
})
watch(deepState, (newVal) => {
console.log('Deep state changed:', newVal)
}, { deep: true })
return {
count,
user,
deepState
}
}
}
computed计算属性的高级用法
import { ref, computed } from 'vue'
export default {
setup() {
const firstName = ref('John')
const lastName = ref('Doe')
const age = ref(30)
// 基本计算属性
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`
})
// 带getter和setter的计算属性
const displayName = computed({
get: () => {
return `${firstName.value} ${lastName.value}`
},
set: (value) => {
const names = value.split(' ')
firstName.value = names[0]
lastName.value = names[1]
}
})
// 复杂计算属性
const userStatus = computed(() => {
if (age.value < 18) return 'minor'
if (age.value < 65) return 'adult'
return 'senior'
})
return {
firstName,
lastName,
age,
fullName,
displayName,
userStatus
}
}
}
组件复用高级技巧
组合函数的创建与使用
组合函数是Composition API中实现逻辑复用的核心机制。通过创建可复用的组合函数,我们可以将通用的逻辑封装起来,供多个组件使用。
// composables/useCounter.js
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const increment = () => count.value++
const decrement = () => count.value--
const reset = () => count.value = initialValue
const doubleCount = computed(() => count.value * 2)
return {
count,
increment,
decrement,
reset,
doubleCount
}
}
// composables/useLocalStorage.js
import { ref, watch } from 'vue'
export function useLocalStorage(key, defaultValue) {
const value = ref(defaultValue)
// 从localStorage初始化
const savedValue = localStorage.getItem(key)
if (savedValue) {
value.value = JSON.parse(savedValue)
}
// 监听变化并保存到localStorage
watch(value, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue))
}, { deep: true })
return value
}
// composables/useFetch.js
import { ref, watch } from 'vue'
export function useFetch(url) {
const data = ref(null)
const loading = ref(false)
const error = ref(null)
const fetchData = async () => {
loading.value = true
error.value = null
try {
const response = await fetch(url)
data.value = await response.json()
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
// 自动执行fetch
watch(() => url, fetchData, { immediate: true })
return {
data,
loading,
error,
refetch: fetchData
}
}
实际应用示例:购物车组件
<!-- Cart.vue -->
<template>
<div class="cart">
<h2>购物车</h2>
<div v-if="loading">加载中...</div>
<div v-else-if="error">{{ error }}</div>
<div v-else>
<div v-for="item in cartItems" :key="item.id" class="cart-item">
<span>{{ item.name }}</span>
<span>数量: {{ item.quantity }}</span>
<span>价格: ¥{{ item.price }}</span>
<button @click="removeItem(item.id)">删除</button>
</div>
<div class="cart-total">
<p>总计: ¥{{ total }}</p>
<button @click="checkout">结算</button>
</div>
</div>
</div>
</template>
<script>
import { computed } from 'vue'
import { useCart } from '@/composables/useCart'
export default {
name: 'Cart',
setup() {
const {
cartItems,
loading,
error,
removeItem,
checkout
} = useCart()
const total = computed(() => {
return cartItems.value.reduce((sum, item) => {
return sum + (item.price * item.quantity)
}, 0)
})
return {
cartItems,
loading,
error,
removeItem,
checkout,
total
}
}
}
</script>
<style scoped>
.cart {
padding: 20px;
}
.cart-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
border: 1px solid #eee;
margin-bottom: 10px;
}
.cart-total {
margin-top: 20px;
padding-top: 20px;
border-top: 2px solid #eee;
}
</style>
组合函数的高级用法
// composables/useForm.js
import { ref, reactive } from 'vue'
export function useForm(initialData = {}) {
const formData = reactive({ ...initialData })
const errors = ref({})
const isSubmitting = ref(false)
const validate = (rules) => {
const newErrors = {}
Object.keys(rules).forEach(field => {
const rule = rules[field]
const value = formData[field]
if (rule.required && !value) {
newErrors[field] = `${field} 是必填项`
}
if (rule.minLength && value && value.length < rule.minLength) {
newErrors[field] = `${field} 长度不能少于${rule.minLength}位`
}
if (rule.pattern && value && !rule.pattern.test(value)) {
newErrors[field] = `${field} 格式不正确`
}
})
errors.value = newErrors
return Object.keys(newErrors).length === 0
}
const submit = async (submitFn) => {
if (!validate()) return
isSubmitting.value = true
try {
const result = await submitFn(formData)
return result
} catch (error) {
console.error('提交失败:', error)
throw error
} finally {
isSubmitting.value = false
}
}
const reset = () => {
Object.keys(formData).forEach(key => {
formData[key] = ''
})
errors.value = {}
}
return {
formData,
errors,
isSubmitting,
validate,
submit,
reset
}
}
// composables/usePagination.js
import { ref, computed } from 'vue'
export function usePagination(data, pageSize = 10) {
const currentPage = ref(1)
const _pageSize = ref(pageSize)
const totalPages = computed(() => {
return Math.ceil(data.value.length / _pageSize.value)
})
const paginatedData = computed(() => {
const start = (currentPage.value - 1) * _pageSize.value
const end = start + _pageSize.value
return data.value.slice(start, end)
})
const goToPage = (page) => {
if (page >= 1 && page <= totalPages.value) {
currentPage.value = page
}
}
const nextPage = () => {
if (currentPage.value < totalPages.value) {
currentPage.value++
}
}
const prevPage = () => {
if (currentPage.value > 1) {
currentPage.value--
}
}
const setPageSize = (size) => {
_pageSize.value = size
currentPage.value = 1
}
return {
currentPage,
totalPages,
paginatedData,
goToPage,
nextPage,
prevPage,
setPageSize
}
}
状态管理高级技巧
基于Composition API的状态管理
// stores/useGlobalStore.js
import { ref, readonly } from 'vue'
// 全局状态管理
const user = ref(null)
const theme = ref('light')
const notifications = ref([])
// 状态操作方法
const setUser = (userData) => {
user.value = userData
}
const setTheme = (newTheme) => {
theme.value = newTheme
}
const addNotification = (notification) => {
notifications.value.push({
id: Date.now(),
...notification,
timestamp: new Date()
})
}
const removeNotification = (id) => {
notifications.value = notifications.value.filter(n => n.id !== id)
}
const clearNotifications = () => {
notifications.value = []
}
// 提供只读状态访问
const readonlyUser = readonly(user)
const readonlyTheme = readonly(theme)
export function useGlobalStore() {
return {
// 状态
user: readonlyUser,
theme: readonlyTheme,
notifications: readonly(notifications),
// 方法
setUser,
setTheme,
addNotification,
removeNotification,
clearNotifications
}
}
复杂状态管理示例:用户管理面板
<!-- UserManagement.vue -->
<template>
<div class="user-management">
<div class="user-header">
<h2>用户管理</h2>
<button @click="openCreateModal">添加用户</button>
</div>
<div class="filters">
<input v-model="searchTerm" placeholder="搜索用户..." />
<select v-model="filterRole">
<option value="">所有角色</option>
<option value="admin">管理员</option>
<option value="user">普通用户</option>
</select>
</div>
<div class="users-table">
<table>
<thead>
<tr>
<th>用户名</th>
<th>邮箱</th>
<th>角色</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="user in filteredUsers" :key="user.id">
<td>{{ user.username }}</td>
<td>{{ user.email }}</td>
<td>{{ user.role }}</td>
<td>
<span :class="user.active ? 'status-active' : 'status-inactive'">
{{ user.active ? '活跃' : '非活跃' }}
</span>
</td>
<td>
<button @click="editUser(user)">编辑</button>
<button @click="toggleUserStatus(user)">切换状态</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="pagination">
<button @click="prevPage" :disabled="currentPage === 1">上一页</button>
<span>第 {{ currentPage }} 页,共 {{ totalPages }} 页</span>
<button @click="nextPage" :disabled="currentPage === totalPages">下一页</button>
</div>
<!-- 模态框 -->
<UserModal
v-if="showModal"
:user="editingUser"
:is-editing="isEditing"
@save="saveUser"
@close="closeModal"
/>
</div>
</template>
<script>
import {
ref,
computed,
watch
} from 'vue'
import { usePagination } from '@/composables/usePagination'
import { useGlobalStore } from '@/stores/useGlobalStore'
import UserModal from './UserModal.vue'
export default {
name: 'UserManagement',
components: {
UserModal
},
setup() {
const { user: currentUser } = useGlobalStore()
const users = ref([])
const searchTerm = ref('')
const filterRole = ref('')
const showModal = ref(false)
const isEditing = ref(false)
const editingUser = ref(null)
// 模拟数据加载
const loadUsers = async () => {
// 这里可以是API调用
users.value = [
{ id: 1, username: 'admin', email: 'admin@example.com', role: 'admin', active: true },
{ id: 2, username: 'user1', email: 'user1@example.com', role: 'user', active: true },
{ id: 3, username: 'user2', email: 'user2@example.com', role: 'user', active: false }
]
}
// 加载用户数据
loadUsers()
// 分页逻辑
const {
currentPage,
totalPages,
paginatedData: paginatedUsers,
nextPage,
prevPage
} = usePagination(users, 5)
// 过滤用户
const filteredUsers = computed(() => {
let result = users.value
if (searchTerm.value) {
const term = searchTerm.value.toLowerCase()
result = result.filter(user =>
user.username.toLowerCase().includes(term) ||
user.email.toLowerCase().includes(term)
)
}
if (filterRole.value) {
result = result.filter(user => user.role === filterRole.value)
}
return result
})
// 编辑用户
const editUser = (user) => {
editingUser.value = { ...user }
isEditing.value = true
showModal.value = true
}
// 打开创建用户模态框
const openCreateModal = () => {
editingUser.value = { username: '', email: '', role: 'user', active: true }
isEditing.value = false
showModal.value = true
}
// 保存用户
const saveUser = async (userData) => {
if (isEditing.value) {
// 更新用户
const index = users.value.findIndex(u => u.id === userData.id)
if (index !== -1) {
users.value[index] = userData
}
} else {
// 创建用户
const newUser = {
...userData,
id: Date.now()
}
users.value.push(newUser)
}
closeModal()
}
// 关闭模态框
const closeModal = () => {
showModal.value = false
editingUser.value = null
}
// 切换用户状态
const toggleUserStatus = (user) => {
const index = users.value.findIndex(u => u.id === user.id)
if (index !== -1) {
users.value[index].active = !users.value[index].active
}
}
return {
// 数据
searchTerm,
filterRole,
users,
filteredUsers,
currentPage,
totalPages,
showModal,
isEditing,
editingUser,
// 方法
nextPage,
prevPage,
editUser,
openCreateModal,
saveUser,
closeModal,
toggleUserStatus
}
}
}
</script>
<style scoped>
.user-management {
padding: 20px;
}
.user-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.filters {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.users-table {
margin-bottom: 20px;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
}
.status-active {
color: green;
font-weight: bold;
}
.status-inactive {
color: red;
font-weight: bold;
}
</style>
性能优化与最佳实践
响应式数据的优化
import { ref, reactive, computed, watch, watchEffect } from 'vue'
export default {
setup() {
// 1. 合理使用ref和reactive
const count = ref(0) // 基本类型用ref
const state = reactive({}) // 对象类型用reactive
// 2. 使用computed优化计算属性
const expensiveComputation = computed(() => {
// 复杂计算逻辑
return someExpensiveOperation()
})
// 3. 合理使用watch
// 避免不必要的监听
const watchOptions = {
immediate: false, // 不立即执行
deep: false, // 不深度监听
flush: 'post' // 异步执行
}
watch(count, (newVal, oldVal) => {
// 监听逻辑
}, watchOptions)
// 4. 使用watchEffect自动追踪依赖
watchEffect(() => {
// 自动追踪依赖,无需手动指定
console.log(count.value)
})
return {
count,
expensiveComputation
}
}
}
组件性能优化
<!-- OptimizedComponent.vue -->
<template>
<div class="optimized-component">
<div v-for="item in optimizedList" :key="item.id" class="item">
{{ item.name }}
</div>
</div>
</template>
<script>
import {
ref,
computed,
defineAsyncComponent
} from 'vue'
export default {
name: 'OptimizedComponent',
setup() {
const items = ref([])
// 使用计算属性缓存结果
const optimizedList = computed(() => {
return items.value.filter(item => item.visible)
})
// 异步组件加载
const AsyncComponent = defineAsyncComponent(() =>
import('./AsyncComponent.vue')
)
// 防抖处理
const debounce = (func, delay) => {
let timeoutId
return (...args) => {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => func.apply(this, args), delay)
}
}
const debouncedSearch = debounce((query) => {
// 搜索逻辑
}, 300)
return {
items,
optimizedList,
AsyncComponent,
debouncedSearch
}
}
}
</script>
实际项目应用案例
电商商品列表组件
<!-- ProductList.vue -->
<template>
<div class="product-list">
<div class="filters">
<select v-model="selectedCategory">
<option value="">所有分类</option>
<option v-for="category in categories" :key="category.id" :value="category.id">
{{ category.name }}
</option>
</select>
<input v-model="searchQuery" placeholder="搜索商品..." />
<div class="price-filters">
<input v-model="minPrice" type="number" placeholder="最低价格" />
<input v-model="maxPrice" type="number" placeholder="最高价格" />
</div>
</div>
<div class="loading" v-if="loading">加载中...</div>
<div class="products-grid" v-else-if="products.length">
<ProductCard
v-for="product in paginatedProducts"
:key="product.id"
:product="product"
@add-to-cart="handleAddToCart"
/>
</div>
<div class="no-products" v-else-if="!loading && products.length === 0">
暂无商品
</div>
<div class="pagination" v-if="totalPages > 1">
<button
v-for="page in paginationPages"
:key="page"
:class="{ active: currentPage === page }"
@click="goToPage(page)"
>
{{ page }}
</button>
</div>
</div>
</template>
<script>
import {
ref,
computed,
watch,
onMounted
} from 'vue'
import { usePagination } from '@/composables/usePagination'
import { useCart } from '@/composables/useCart'
import ProductCard from './ProductCard.vue'
export default {
name: 'ProductList',
components: {
ProductCard
},
setup() {
const products = ref([])
const loading = ref(false)
const searchQuery = ref('')
const selectedCategory = ref('')
const minPrice = ref('')
const maxPrice = ref('')
const categories = ref([
{ id: 'electronics', name: '电子产品' },
{ id: 'clothing', name: '服装' },
{ id: 'books', name: '图书' }
])
// 模拟API调用
const fetchProducts = async () => {
loading.value = true
try {
// 模拟API延迟
await new Promise(resolve => setTimeout(resolve, 1000))
// 模拟产品数据
products.value = [
{ id: 1, name: 'iPhone 14', price: 5999, category: 'electronics', image: '/images/iphone.jpg' },
{ id: 2, name: 'MacBook Pro', price: 12999, category: 'electronics', image: '/images/macbook.jpg' },
{ id: 3, name: 'T恤衫', price: 99, category: 'clothing', image: '/images/tshirt.jpg' },
{ id: 4, name: 'JavaScript高级程序设计', price: 89, category: 'books', image: '/images/js-book.jpg' }
]
} catch (error) {
console.error('获取产品失败:', error)
} finally {
loading.value = false
}
}
// 过滤产品
const filteredProducts = computed(() => {
let result = products.value
if (searchQuery.value) {
const term = searchQuery.value.toLowerCase()
result = result.filter(product =>
product.name.toLowerCase().includes(term)
)
}
if (selectedCategory.value) {
result = result.filter(product =>
product.category === selectedCategory.value
)
}
if (minPrice.value) {
result = result.filter(product =>
product.price >= parseFloat(minPrice.value)
)
}
if (maxPrice.value) {
result = result.filter(product =>
product.price <= parseFloat(maxPrice.value)
)
}
return result
})
// 分页处理
const {
currentPage,
totalPages,
paginatedData: paginatedProducts,
goToPage
} = usePagination(filteredProducts, 6)
// 计算分页按钮
const paginationPages = computed(() => {
const pages = []
const maxVisible = 5
const start = Math.max(1, currentPage.value - Math.floor(maxVisible / 2))
评论 (0)