引言
Vue 3的发布带来了革命性的变化,其中最引人注目的就是Composition API的引入。这个新特性彻底改变了我们编写Vue组件的方式,让代码更加灵活、可复用和易于维护。在本文中,我们将深入探讨Composition API的核心概念,并通过实际案例演示如何构建现代化的Vue组件。
Vue 3 Composition API基础概念
什么是Composition API
Composition API是Vue 3提供的一种新的组件逻辑组织方式。它允许我们以函数的形式组织组件逻辑,而不是传统的选项式API(Options API)。这种方式更加灵活,能够更好地处理复杂组件逻辑的复用和组合。
Composition API的核心函数
Composition API提供了多个核心函数来处理响应式数据、生命周期钩子等:
import {
ref,
reactive,
computed,
watch,
watchEffect,
onMounted,
onUpdated,
onUnmounted
} from 'vue'
响应式数据管理
Ref vs Reactive
在Composition API中,我们主要使用ref和reactive来创建响应式数据:
import { ref, reactive } from 'vue'
// 使用ref创建响应式数据
const count = ref(0)
const name = ref('Vue')
// 使用reactive创建响应式对象
const state = reactive({
count: 0,
name: 'Vue',
user: {
firstName: 'John',
lastName: 'Doe'
}
})
// 在模板中使用时,ref需要通过.value访问
// 在组件逻辑中可以直接使用
console.log(count.value) // 0
console.log(state.count) // 0
复杂数据结构的响应式处理
import { ref, reactive, toRefs } from 'vue'
export default {
setup() {
// 创建复杂的响应式对象
const user = reactive({
profile: {
name: 'Alice',
age: 25,
email: 'alice@example.com'
},
preferences: {
theme: 'dark',
language: 'zh-CN'
}
})
// 使用toRefs将响应式对象转换为可解构的ref
const { profile, preferences } = toRefs(user)
return {
user,
profile,
preferences
}
}
}
生命周期钩子的使用
组件生命周期管理
import {
onMounted,
onUpdated,
onUnmounted,
onBeforeMount,
onBeforeUpdate,
onBeforeUnmount
} from 'vue'
export default {
setup() {
// 组件挂载前
onBeforeMount(() => {
console.log('组件即将挂载')
})
// 组件挂载后
onMounted(() => {
console.log('组件已挂载')
// 可以在这里执行DOM操作
const element = document.getElementById('myElement')
if (element) {
// 执行一些DOM相关的操作
}
})
// 组件更新前
onBeforeUpdate(() => {
console.log('组件即将更新')
})
// 组件更新后
onUpdated(() => {
console.log('组件已更新')
})
// 组件卸载前
onBeforeUnmount(() => {
console.log('组件即将卸载')
})
// 组件卸载后
onUnmounted(() => {
console.log('组件已卸载')
})
return {}
}
}
计算属性和监听器
计算属性的创建
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 userSummary = computed(() => {
return `姓名: ${fullName.value}, 年龄: ${age.value}岁`
})
return {
firstName,
lastName,
age,
fullName,
displayName,
userSummary
}
}
}
监听器的使用
import { ref, watch, watchEffect } from 'vue'
export default {
setup() {
const count = ref(0)
const name = ref('Vue')
const user = ref({ age: 25 })
// 基本监听器
watch(count, (newVal, oldVal) => {
console.log(`count从${oldVal}变为${newVal}`)
})
// 监听多个源
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
console.log(`count: ${oldCount} -> ${newCount}, name: ${oldName} -> ${newName}`)
})
// 深度监听对象
watch(user, (newVal, oldVal) => {
console.log('user对象发生变化')
}, { deep: true })
// 立即执行的监听器
watch(count, (newVal) => {
console.log(`立即执行: ${newVal}`)
}, { immediate: true })
// watchEffect - 自动追踪依赖
watchEffect(() => {
console.log(`当前count值: ${count.value}`)
console.log(`当前name值: ${name.value}`)
// 这里会自动追踪count和name的依赖关系
})
return {
count,
name,
user
}
}
}
组件逻辑复用
自定义组合函数
// 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 storedValue = localStorage.getItem(key)
if (storedValue) {
value.value = JSON.parse(storedValue)
}
// 监听值变化并同步到localStorage
watch(value, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue))
}, { deep: true })
return value
}
// composables/useApi.js
import { ref, reactive } from 'vue'
export function useApi() {
const data = ref(null)
const loading = ref(false)
const error = ref(null)
const fetchData = async (url) => {
try {
loading.value = true
error.value = null
const response = await fetch(url)
data.value = await response.json()
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
return {
data,
loading,
error,
fetchData
}
}
使用自定义组合函数
<template>
<div>
<h2>计数器示例</h2>
<p>当前计数: {{ counter.count }}</p>
<p>双倍计数: {{ counter.doubleCount }}</p>
<button @click="counter.increment">增加</button>
<button @click="counter.decrement">减少</button>
<button @click="counter.reset">重置</button>
<h2>本地存储示例</h2>
<input v-model="localStorageValue" placeholder="输入内容">
<p>存储的值: {{ localStorageValue }}</p>
<h2>API数据获取</h2>
<div v-if="api.loading">加载中...</div>
<div v-else-if="api.error">{{ api.error }}</div>
<div v-else-if="api.data">
<pre>{{ JSON.stringify(api.data, null, 2) }}</pre>
</div>
<button @click="api.fetchData('https://jsonplaceholder.typicode.com/posts/1')">
获取数据
</button>
</div>
</template>
<script>
import { useCounter } from './composables/useCounter'
import { useLocalStorage } from './composables/useLocalStorage'
import { useApi } from './composables/useApi'
export default {
setup() {
// 使用计数器组合函数
const counter = useCounter(0)
// 使用本地存储组合函数
const localStorageValue = useLocalStorage('myKey', '默认值')
// 使用API组合函数
const api = useApi()
return {
counter,
localStorageValue,
api
}
}
}
</script>
复杂组件开发实战
表单处理组件
<template>
<form @submit.prevent="handleSubmit">
<div class="form-group">
<label>用户名:</label>
<input
v-model="formData.username"
type="text"
:class="{ error: errors.username }"
/>
<span v-if="errors.username" class="error-message">{{ errors.username }}</span>
</div>
<div class="form-group">
<label>邮箱:</label>
<input
v-model="formData.email"
type="email"
:class="{ error: errors.email }"
/>
<span v-if="errors.email" class="error-message">{{ errors.email }}</span>
</div>
<div class="form-group">
<label>密码:</label>
<input
v-model="formData.password"
type="password"
:class="{ error: errors.password }"
/>
<span v-if="errors.password" class="error-message">{{ errors.password }}</span>
</div>
<div class="form-group">
<label>确认密码:</label>
<input
v-model="formData.confirmPassword"
type="password"
:class="{ error: errors.confirmPassword }"
/>
<span v-if="errors.confirmPassword" class="error-message">{{ errors.confirmPassword }}</span>
</div>
<button type="submit" :disabled="isSubmitting">提交</button>
</form>
</template>
<script>
import { ref, reactive, computed } from 'vue'
export default {
setup() {
const formData = reactive({
username: '',
email: '',
password: '',
confirmPassword: ''
})
const errors = reactive({})
const isSubmitting = ref(false)
// 验证规则
const validateField = (field, value) => {
switch (field) {
case 'username':
if (!value) return '用户名不能为空'
if (value.length < 3) return '用户名至少3个字符'
break
case 'email':
if (!value) return '邮箱不能为空'
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(value)) return '邮箱格式不正确'
break
case 'password':
if (!value) return '密码不能为空'
if (value.length < 6) return '密码至少6个字符'
break
case 'confirmPassword':
if (!value) return '请确认密码'
if (value !== formData.password) return '两次输入的密码不一致'
break
}
return ''
}
// 全局验证
const validateForm = () => {
const newErrors = {}
Object.keys(formData).forEach(key => {
const error = validateField(key, formData[key])
if (error) {
newErrors[key] = error
}
})
return newErrors
}
// 实时验证
const validate = (field) => {
const error = validateField(field, formData[field])
errors[field] = error
}
// 监听表单字段变化
Object.keys(formData).forEach(key => {
if (key !== 'confirmPassword') { // confirmPassword在验证时特殊处理
watch(() => formData[key], () => {
validate(key)
})
}
})
// 处理表单提交
const handleSubmit = async () => {
const newErrors = validateForm()
if (Object.keys(newErrors).length > 0) {
Object.assign(errors, newErrors)
return
}
isSubmitting.value = true
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000))
console.log('表单提交成功:', formData)
// 这里可以添加实际的API调用逻辑
} catch (error) {
console.error('表单提交失败:', error)
} finally {
isSubmitting.value = false
}
}
return {
formData,
errors,
isSubmitting,
handleSubmit
}
}
}
</script>
<style scoped>
.form-group {
margin-bottom: 1rem;
}
.error-message {
color: red;
font-size: 0.8rem;
}
input.error {
border-color: red;
}
</style>
数据表格组件
<template>
<div class="data-table">
<!-- 搜索和过滤 -->
<div class="table-controls">
<input
v-model="searchQuery"
placeholder="搜索..."
class="search-input"
/>
<select v-model="filterStatus" class="filter-select">
<option value="">全部状态</option>
<option value="active">活跃</option>
<option value="inactive">非活跃</option>
</select>
</div>
<!-- 表格 -->
<table class="table">
<thead>
<tr>
<th v-for="column in columns" :key="column.key" @click="sort(column.key)">
{{ column.title }}
<span v-if="sortField === column.key" class="sort-indicator">
{{ sortOrder === 'asc' ? '↑' : '↓' }}
</span>
</th>
</tr>
</thead>
<tbody>
<tr v-for="row in paginatedData" :key="row.id">
<td v-for="column in columns" :key="column.key">
{{ formatValue(row[column.key], column) }}
</td>
</tr>
</tbody>
</table>
<!-- 分页 -->
<div class="pagination">
<button
@click="currentPage--"
:disabled="currentPage === 1"
class="page-btn"
>
上一页
</button>
<span>第 {{ currentPage }} 页,共 {{ totalPages }} 页</span>
<button
@click="currentPage++"
:disabled="currentPage === totalPages"
class="page-btn"
>
下一页
</button>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading">加载中...</div>
</div>
</template>
<script>
import { ref, computed, watch } from 'vue'
export default {
props: {
data: {
type: Array,
required: true
},
columns: {
type: Array,
required: true
}
},
setup(props) {
const searchQuery = ref('')
const filterStatus = ref('')
const sortField = ref('')
const sortOrder = ref('asc')
const currentPage = ref(1)
const pageSize = ref(10)
const loading = ref(false)
// 过滤数据
const filteredData = computed(() => {
let result = [...props.data]
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
result = result.filter(item =>
Object.values(item).some(value =>
value.toString().toLowerCase().includes(query)
)
)
}
if (filterStatus.value) {
result = result.filter(item => item.status === filterStatus.value)
}
return result
})
// 排序数据
const sortedData = computed(() => {
if (!sortField.value) return filteredData.value
return [...filteredData.value].sort((a, b) => {
const aValue = a[sortField.value]
const bValue = b[sortField.value]
if (typeof aValue === 'string') {
const comparison = aValue.localeCompare(bValue)
return sortOrder.value === 'asc' ? comparison : -comparison
} else {
const comparison = aValue - bValue
return sortOrder.value === 'asc' ? comparison : -comparison
}
})
})
// 分页数据
const totalPages = computed(() => {
return Math.ceil(sortedData.value.length / pageSize.value)
})
const paginatedData = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
return sortedData.value.slice(start, start + pageSize.value)
})
// 排序功能
const sort = (field) => {
if (sortField.value === field) {
sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'
} else {
sortField.value = field
sortOrder.value = 'asc'
}
}
// 格式化值
const formatValue = (value, column) => {
if (column.formatter) {
return column.formatter(value)
}
if (typeof value === 'boolean') {
return value ? '是' : '否'
}
return value
}
// 监听分页变化
watch(currentPage, () => {
currentPage.value = Math.max(1, Math.min(currentPage.value, totalPages.value))
})
// 监听数据变化时重置分页
watch(() => props.data, () => {
currentPage.value = 1
})
return {
searchQuery,
filterStatus,
sortField,
sortOrder,
currentPage,
pageSize,
loading,
totalPages,
paginatedData,
sort,
formatValue
}
}
}
</script>
<style scoped>
.data-table {
padding: 1rem;
}
.table-controls {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
align-items: center;
}
.search-input, .filter-select {
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
}
.table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1rem;
}
.table th,
.table td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid #ddd;
}
.table th {
background-color: #f5f5f5;
cursor: pointer;
user-select: none;
}
.sort-indicator {
margin-left: 0.25rem;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
margin-top: 1rem;
}
.page-btn {
padding: 0.5rem 1rem;
border: 1px solid #ddd;
background-color: white;
cursor: pointer;
border-radius: 4px;
}
.page-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.loading {
text-align: center;
padding: 2rem;
}
</style>
性能优化最佳实践
避免不必要的重渲染
import { ref, computed, shallowRef, markRaw } from 'vue'
export default {
setup() {
// 使用shallowRef进行浅层响应式
const shallowData = shallowRef({
name: 'Vue',
version: '3.0'
})
// 对于不希望被响应式的对象,使用markRaw
const rawData = markRaw({
id: 1,
name: 'Vue',
handler: function() {
console.log('处理事件')
}
})
// 使用computed缓存计算结果
const expensiveComputation = computed(() => {
// 模拟耗时计算
let result = 0
for (let i = 0; i < 1000000; i++) {
result += Math.random()
}
return result
})
// 使用watch的回调控制执行时机
const watchOptions = {
flush: 'post', // 在组件更新后执行
deep: true,
immediate: false
}
return {
shallowData,
rawData,
expensiveComputation
}
}
}
组件通信优化
<template>
<div>
<h2>优化的组件通信</h2>
<p>父组件数据: {{ parentData }}</p>
<ChildComponent
:data="parentData"
@update-data="handleUpdate"
/>
</div>
</template>
<script>
import { ref, watch } from 'vue'
import ChildComponent from './ChildComponent.vue'
export default {
components: {
ChildComponent
},
setup() {
const parentData = ref('初始数据')
// 使用watch监听数据变化,避免不必要的重新计算
watch(parentData, (newVal, oldVal) => {
console.log(`父组件数据从 ${oldVal} 变为 ${newVal}`)
}, { deep: true })
const handleUpdate = (data) => {
parentData.value = data
}
return {
parentData,
handleUpdate
}
}
}
</script>
错误处理和调试
统一错误处理机制
import { ref, reactive } from 'vue'
export function useErrorHandler() {
const error = ref(null)
const loading = ref(false)
const handleError = (error) => {
console.error('组件错误:', error)
// 可以在这里添加错误上报逻辑
error.value = error.message || '发生未知错误'
}
const handleAsyncOperation = async (operation) => {
try {
loading.value = true
error.value = null
return await operation()
} catch (err) {
handleError(err)
throw err
} finally {
loading.value = false
}
}
return {
error,
loading,
handleAsyncOperation
}
}
// 在组件中使用
export default {
setup() {
const { error, loading, handleAsyncOperation } = useErrorHandler()
const fetchData = async () => {
await handleAsyncOperation(async () => {
// 模拟API调用
const response = await fetch('/api/data')
if (!response.ok) {
throw new Error('网络请求失败')
}
return response.json()
})
}
return {
error,
loading,
fetchData
}
}
}
总结
Vue 3的Composition API为前端开发带来了革命性的变化。通过本文的介绍,我们看到了:
- 响应式数据管理:
ref和reactive提供了灵活的数据响应式处理方式 - 生命周期钩子:更清晰的组件生命周期管理
- 逻辑复用:通过自定义组合函数实现代码复用
- 复杂组件开发:从表单到数据表格等实际场景的应用
- 性能优化:合理使用响应式特性避免性能问题
- 错误处理:建立统一的错误处理机制
Composition API的核心优势在于它让我们能够以更自然的方式组织代码逻辑,特别是在处理复杂的业务逻辑时,能够显著提升代码的可维护性和复用性。通过合理运用这些技术,我们可以构建出更加现代化、高效且易于维护的Vue应用。
在实际项目中,建议根据具体需求选择合适的API风格,同时注意遵循最佳实践,这样才能充分发挥Vue 3 Composition API的强大功能。

评论 (0)