引言
Vue 3的发布带来了革命性的变化,其中最引人注目的就是Composition API的引入。相比于Vue 2的Options API,Composition API提供了一种更加灵活和强大的方式来组织和管理组件逻辑。本文将深入探讨Composition API的核心概念、使用技巧以及最佳实践,帮助开发者构建更加高效和可维护的前端应用。
什么是Composition API
核心概念
Composition API是Vue 3中引入的一种新的组件逻辑组织方式。它允许开发者将组件的逻辑按照功能进行分组,而不是按照选项类型进行组织。这种组织方式使得代码更加灵活,更容易复用和维护。
在Vue 2中,我们通常按照data、methods、computed、watch等选项来组织组件逻辑。而Composition API则允许我们按照功能模块来组织代码,比如将所有与用户相关的逻辑放在一起,将所有与数据获取相关的逻辑放在一起。
与Options API的区别
// Vue 2 Options API
export default {
data() {
return {
count: 0,
name: 'Vue'
}
},
computed: {
reversedName() {
return this.name.split('').reverse().join('')
}
},
methods: {
increment() {
this.count++
}
},
watch: {
count(newVal, oldVal) {
console.log(`count changed from ${oldVal} to ${newVal}`)
}
}
}
// Vue 3 Composition API
import { ref, computed, watch } from 'vue'
export default {
setup() {
const count = ref(0)
const name = ref('Vue')
const reversedName = computed(() => {
return name.value.split('').reverse().join('')
})
const increment = () => {
count.value++
}
watch(count, (newVal, oldVal) => {
console.log(`count changed from ${oldVal} to ${newVal}`)
})
return {
count,
name,
reversedName,
increment
}
}
}
响应式数据处理基础
ref和reactive的基础使用
在Composition API中,响应式数据的处理主要依赖于ref和reactive两个核心函数。
import { ref, reactive } from 'vue'
// 使用ref创建响应式数据
const count = ref(0)
const name = ref('Vue')
const isActive = ref(true)
// 使用reactive创建响应式对象
const user = reactive({
name: 'Vue',
age: 3,
email: 'vue@vuejs.org'
})
// 访问响应式数据
console.log(count.value) // 0
console.log(name.value) // 'Vue'
// 修改响应式数据
count.value = 1
name.value = 'Vue 3'
ref的深层理解
ref会将传入的值包装成一个响应式对象,这个对象有一个value属性指向原始值。对于基本类型数据,ref是必需的;而对于对象类型数据,我们通常使用reactive。
import { ref, reactive } from 'vue'
// 基本类型使用ref
const count = ref(0)
const message = ref('Hello')
// 对象类型使用reactive
const userInfo = reactive({
name: 'Vue',
age: 3,
address: {
city: 'Shanghai',
country: 'China'
}
})
// 数组使用reactive
const items = reactive([1, 2, 3, 4])
// ref在模板中的使用
// <template>
// <p>{{ count }}</p>
// <p>{{ message }}</p>
// <p>{{ userInfo.name }}</p>
// </template>
reactive的使用技巧
reactive函数可以将对象转换为响应式对象,但需要注意的是,它不能替代ref来处理基本类型数据。
import { reactive } from 'vue'
// 创建响应式对象
const state = reactive({
count: 0,
name: 'Vue',
user: {
profile: {
avatar: 'avatar.png'
}
}
})
// 修改嵌套对象属性
state.user.profile.avatar = 'new-avatar.png'
state.count = 10
// 注意:直接替换整个对象会丢失响应性
// state = reactive({ count: 5 }) // 这样做会丢失响应性
组合式函数的封装与复用
创建组合式函数的基本模式
组合式函数是Vue 3中实现逻辑复用的核心机制。它们本质上是函数,接收参数并返回响应式数据和方法。
// composables/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
}
}
// 在组件中使用
import { useCounter } from '@/composables/useCounter'
export default {
setup() {
const { count, increment, decrement, reset } = useCounter(10)
return {
count,
increment,
decrement,
reset
}
}
}
复杂组合式函数示例
// composables/useApi.js
import { ref, watch } from 'vue'
export function useApi(url, options = {}) {
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, options)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
data.value = await response.json()
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
// 自动获取数据
if (options.autoFetch !== false) {
fetchData()
}
// 监听URL变化,重新获取数据
watch(url, fetchData)
return {
data,
loading,
error,
refetch: fetchData
}
}
// 在组件中使用
export default {
setup() {
const { data, loading, error, refetch } = useApi('/api/users')
return {
data,
loading,
error,
refetch
}
}
}
组合式函数中的副作用处理
// composables/useLocalStorage.js
import { ref, watch } from 'vue'
export function useLocalStorage(key, defaultValue) {
const value = ref(defaultValue)
// 从localStorage初始化
const initValue = localStorage.getItem(key)
if (initValue) {
try {
value.value = JSON.parse(initValue)
} catch (e) {
console.error('Failed to parse localStorage value:', e)
}
}
// 监听值变化并保存到localStorage
watch(value, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue))
}, { deep: true })
return value
}
// 使用示例
export default {
setup() {
const userPreferences = useLocalStorage('user-preferences', {
theme: 'light',
language: 'zh-CN'
})
return {
userPreferences
}
}
}
状态管理与跨组件通信
简单的状态管理
对于简单的应用状态管理,我们可以使用reactive来创建全局状态。
// stores/appStore.js
import { reactive } from 'vue'
export const appStore = reactive({
user: null,
theme: 'light',
language: 'zh-CN',
notifications: [],
setUser(user) {
this.user = user
},
setTheme(theme) {
this.theme = theme
},
addNotification(notification) {
this.notifications.push({
id: Date.now(),
...notification
})
},
removeNotification(id) {
this.notifications = this.notifications.filter(n => n.id !== id)
}
})
// 在组件中使用
import { appStore } from '@/stores/appStore'
export default {
setup() {
const { user, theme, notifications } = appStore
const login = (userData) => {
appStore.setUser(userData)
}
const changeTheme = (newTheme) => {
appStore.setTheme(newTheme)
}
return {
user,
theme,
notifications,
login,
changeTheme
}
}
}
使用provide和inject进行状态传递
// parent.vue
import { provide, reactive } from 'vue'
export default {
setup() {
const globalState = reactive({
user: null,
theme: 'light',
loading: false
})
provide('globalState', globalState)
const updateUser = (user) => {
globalState.user = user
}
return {
updateUser
}
}
}
// child.vue
import { inject } from 'vue'
export default {
setup() {
const globalState = inject('globalState')
const changeTheme = (theme) => {
globalState.theme = theme
}
return {
globalState,
changeTheme
}
}
}
复杂状态管理的实现
// stores/userStore.js
import { reactive, readonly } from 'vue'
const state = reactive({
currentUser: null,
isAuthenticated: false,
permissions: [],
profile: null
})
const getters = {
hasPermission: (permission) => {
return state.permissions.includes(permission)
},
canAccess: (route) => {
// 实现路由访问控制逻辑
return true
}
}
const actions = {
async login(credentials) {
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(credentials)
})
const data = await response.json()
state.currentUser = data.user
state.isAuthenticated = true
state.permissions = data.permissions
state.profile = data.profile
return data
} catch (error) {
throw new Error('Login failed')
}
},
logout() {
state.currentUser = null
state.isAuthenticated = false
state.permissions = []
state.profile = null
}
}
// 提供只读状态访问
export const useUserStore = () => {
return {
state: readonly(state),
getters,
actions
}
}
高级特性与最佳实践
计算属性和监听器的高级用法
import { ref, computed, watch, watchEffect } from 'vue'
export default {
setup() {
const firstName = ref('')
const lastName = ref('')
const age = ref(0)
// 计算属性
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`
})
const isAdult = computed(() => {
return age.value >= 18
})
// 带有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 watchHandler = (newVal, oldVal) => {
console.log(`Value changed from ${oldVal} to ${newVal}`)
}
watch(firstName, watchHandler)
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
console.log(`Name changed from ${oldFirst} ${oldLast} to ${newFirst} ${newLast}`)
})
// watchEffect
const watchEffectHandler = watchEffect(() => {
console.log(`Full name is: ${fullName.value}`)
console.log(`Is adult: ${isAdult.value}`)
})
// 清理副作用
const cleanup = watchEffect((onInvalidate) => {
const timer = setTimeout(() => {
console.log('Timer completed')
}, 1000)
onInvalidate(() => {
clearTimeout(timer)
console.log('Timer cleared')
})
})
return {
firstName,
lastName,
age,
fullName,
isAdult,
displayName,
watchEffectHandler
}
}
}
异步操作和错误处理
import { ref, reactive } from 'vue'
export default {
setup() {
const loading = ref(false)
const error = ref(null)
const data = ref(null)
const fetchData = async (url) => {
loading.value = true
error.value = null
try {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const result = await response.json()
data.value = result
} catch (err) {
error.value = err.message
console.error('Fetch error:', err)
} finally {
loading.value = false
}
}
const handleAsyncOperation = async () => {
try {
const result = await someAsyncFunction()
// 处理结果
return result
} catch (err) {
// 统一错误处理
error.value = err.message
throw err
}
}
return {
loading,
error,
data,
fetchData,
handleAsyncOperation
}
}
}
性能优化技巧
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
export default {
setup() {
const count = ref(0)
const list = ref([])
// 使用computed缓存计算结果
const expensiveValue = computed(() => {
// 模拟耗时计算
return list.value.reduce((sum, item) => sum + item.value, 0)
})
// 优化监听器
const optimizedWatch = watch(
() => count.value,
(newVal, oldVal) => {
// 只在必要时执行
if (newVal > 100) {
console.log('Count is large')
}
},
{ flush: 'post' } // 在DOM更新后执行
)
// 防抖和节流
const debouncedHandler = debounce((value) => {
console.log('Debounced:', value)
}, 300)
const throttledHandler = throttle((value) => {
console.log('Throttled:', value)
}, 1000)
// 生命周期钩子
onMounted(() => {
console.log('Component mounted')
// 初始化操作
})
onUnmounted(() => {
console.log('Component unmounted')
// 清理操作
})
return {
count,
list,
expensiveValue,
debouncedHandler,
throttledHandler
}
}
}
// 防抖函数
function debounce(func, wait) {
let timeout
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout)
func(...args)
}
clearTimeout(timeout)
timeout = setTimeout(later, wait)
}
}
// 节流函数
function throttle(func, limit) {
let inThrottle
return function(...args) {
if (!inThrottle) {
func(...args)
inThrottle = true
setTimeout(() => inThrottle = false, limit)
}
}
}
实际项目应用案例
电商商品列表组件
<template>
<div class="product-list">
<div class="controls">
<input v-model="searchQuery" placeholder="搜索商品..." />
<select v-model="sortBy">
<option value="name">按名称排序</option>
<option value="price">按价格排序</option>
<option value="rating">按评分排序</option>
</select>
<button @click="loadMore">加载更多</button>
</div>
<div v-if="loading" class="loading">加载中...</div>
<div v-if="error" class="error">{{ error }}</div>
<div class="products">
<product-card
v-for="product in filteredProducts"
:key="product.id"
:product="product"
@favorite="toggleFavorite"
/>
</div>
<div v-if="hasMore" class="load-more">
<button @click="loadMore">加载更多</button>
</div>
</div>
</template>
<script>
import { ref, computed, watch } from 'vue'
import { useApi } from '@/composables/useApi'
import { useLocalStorage } from '@/composables/useLocalStorage'
export default {
setup() {
// 响应式数据
const searchQuery = ref('')
const sortBy = ref('name')
const page = ref(1)
const favorites = useLocalStorage('favorites', [])
// API调用
const { data, loading, error, refetch } = useApi(
`/api/products?page=${page.value}&limit=20`
)
// 计算属性
const products = computed(() => data?.products || [])
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)
)
}
// 排序
result.sort((a, b) => {
switch (sortBy.value) {
case 'price':
return a.price - b.price
case 'rating':
return b.rating - a.rating
default:
return a.name.localeCompare(b.name)
}
})
return result
})
const hasMore = computed(() => {
return data?.hasMore || false
})
// 方法
const loadMore = () => {
page.value++
refetch()
}
const toggleFavorite = (productId) => {
const index = favorites.value.indexOf(productId)
if (index > -1) {
favorites.value.splice(index, 1)
} else {
favorites.value.push(productId)
}
}
// 监听器
watch([searchQuery, sortBy], () => {
page.value = 1
refetch()
})
return {
searchQuery,
sortBy,
loading,
error,
filteredProducts,
hasMore,
loadMore,
toggleFavorite
}
}
}
</script>
<style scoped>
.product-list {
padding: 20px;
}
.controls {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.products {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
}
.loading, .error {
text-align: center;
padding: 20px;
}
.load-more {
text-align: center;
margin-top: 20px;
}
</style>
用户管理面板组件
<template>
<div class="user-panel">
<div class="header">
<h2>用户管理</h2>
<button @click="showCreateForm = true">添加用户</button>
</div>
<div class="filters">
<input v-model="filter.name" placeholder="用户名搜索" />
<select v-model="filter.role">
<option value="">所有角色</option>
<option value="admin">管理员</option>
<option value="user">普通用户</option>
<option value="guest">访客</option>
</select>
<button @click="resetFilters">重置</button>
</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.name }}</td>
<td>{{ user.email }}</td>
<td><span class="role-badge" :class="user.role">{{ user.role }}</span></td>
<td>
<span :class="user.isActive ? 'status-active' : 'status-inactive'">
{{ user.isActive ? '活跃' : '非活跃' }}
</span>
</td>
<td class="actions">
<button @click="editUser(user)">编辑</button>
<button @click="deleteUser(user.id)">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
<pagination
:current-page="currentPage"
:total-pages="totalPages"
@page-changed="handlePageChange"
/>
<!-- 用户表单模态框 -->
<user-form
v-if="showCreateForm || editingUser"
:user="editingUser"
@save="saveUser"
@cancel="cancelForm"
/>
</div>
</template>
<script>
import { ref, computed, watch } from 'vue'
import { useApi } from '@/composables/useApi'
import UserForm from './UserForm.vue'
import Pagination from './Pagination.vue'
export default {
components: {
UserForm,
Pagination
},
setup() {
// 响应式数据
const currentPage = ref(1)
const filter = ref({
name: '',
role: ''
})
const showCreateForm = ref(false)
const editingUser = ref(null)
// API调用
const { data, loading, error, refetch } = useApi(
`/api/users?page=${currentPage.value}&limit=10`
)
// 计算属性
const users = computed(() => data?.users || [])
const totalPages = computed(() => data?.totalPages || 1)
const filteredUsers = computed(() => {
let result = users.value
if (filter.value.name) {
const name = filter.value.name.toLowerCase()
result = result.filter(user =>
user.name.toLowerCase().includes(name)
)
}
if (filter.value.role) {
result = result.filter(user =>
user.role === filter.value.role
)
}
return result
})
// 方法
const handlePageChange = (page) => {
currentPage.value = page
refetch()
}
const resetFilters = () => {
filter.value = { name: '', role: '' }
}
const editUser = (user) => {
editingUser.value = { ...user }
showCreateForm.value = true
}
const deleteUser = async (userId) => {
if (confirm('确定要删除这个用户吗?')) {
try {
await fetch(`/api/users/${userId}`, { method: 'DELETE' })
await refetch()
} catch (err) {
console.error('删除用户失败:', err)
}
}
}
const saveUser = async (userData) => {
try {
const method = userData.id ? 'PUT' : 'POST'
const url = userData.id ? `/api/users/${userData.id}` : '/api/users'
await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
})
await refetch()
showCreateForm.value = false
editingUser.value = null
} catch (err) {
console.error('保存用户失败:', err)
}
}
const cancelForm = () => {
showCreateForm.value = false
editingUser.value = null
}
// 监听器
watch([currentPage, filter], () => {
refetch()
})
return {
currentPage,
filter,
showCreateForm,
editingUser,
loading,
error,
filteredUsers,
totalPages,
handlePageChange,
resetFilters,
editUser,
deleteUser,
saveUser,
cancelForm
}
}
}
</script>
<style scoped>
.user-panel {
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.filters {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.users-table {
overflow-x: auto;
margin-bottom: 20px;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
.role-badge {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
}
.role-badge.admin {
background-color: #ff6b6b;
color: white;
}
.role-badge.user {
background-color: #4ecdc4;
color: white;
}
.role-badge.guest {
background-color: #45b7d1;
color: white;
}
.status-active {
color: #28a745;
font-weight: bold;
}
.status-inactive {
color: #dc3545;
font-weight: bold;
}
.actions button {
margin-right: 5px;
padding: 4px 8px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.actions button:first-child {
background-color: #007bff;
color: white;
}
.actions button:last-child {
background-color: #dc3545;
color: white;
}
</style>
总结与展望
Vue 3的Composition API为前端开发带来了革命性的变化,它不仅提供了更加灵活的组件逻辑组织方式,还大大增强了代码的可复用性和可维护性。通过本文的介绍,我们可以看到:
-
响应式数据处理:
ref和reactive的正确使用是基础,理解它们的区别和适用场景至关重要。 -
组合式函数:这是实现逻辑复用的核心机制,通过合理设计

评论 (0)