引言
Vue 3 的发布带来了革命性的变化,其中最引人注目的就是 Composition API 的引入。相比传统的 Options API,Composition API 提供了更灵活、更强大的组件开发方式,特别是在处理复杂逻辑和组件复用方面表现卓越。本文将深入探讨 Vue 3 Composition API 的高级用法,包括组合式函数设计、响应式数据管理、组件间通信策略等,帮助前端开发者构建更灵活、可维护的现代Web应用。
Composition API 核心概念
什么是 Composition API
Composition API 是 Vue 3 中引入的一种新的组件逻辑组织方式。它允许开发者将组件的逻辑按照功能进行组合,而不是按照选项类型进行组织。这种设计模式使得代码更加模块化,易于复用和维护。
// 传统的 Options API
export default {
data() {
return {
count: 0,
message: 'Hello'
}
},
methods: {
increment() {
this.count++
}
},
computed: {
reversedMessage() {
return this.message.split('').reverse().join('')
}
}
}
// Composition API
import { ref, computed } from 'vue'
export default {
setup() {
const count = ref(0)
const message = ref('Hello')
const increment = () => {
count.value++
}
const reversedMessage = computed(() => {
return message.value.split('').reverse().join('')
})
return {
count,
message,
increment,
reversedMessage
}
}
}
setup 函数详解
setup 函数是 Composition API 的入口点,它在组件实例创建之前执行。在这个函数中,你可以访问所有 Composition API 的功能,并返回需要暴露给模板的属性和方法。
import { ref, reactive, computed, watch, onMounted } from 'vue'
export default {
setup(props, context) {
// 接收 props
console.log(props)
// 访问 context
console.log(context.attrs)
console.log(context.emit)
console.log(context.slots)
// 声明响应式数据
const count = ref(0)
const user = reactive({
name: 'John',
age: 30
})
// 计算属性
const doubledCount = computed(() => count.value * 2)
// 监听器
watch(count, (newVal, oldVal) => {
console.log(`count changed from ${oldVal} to ${newVal}`)
})
// 生命周期钩子
onMounted(() => {
console.log('Component mounted')
})
// 返回给模板使用的数据和方法
return {
count,
user,
doubledCount,
increment: () => count.value++
}
}
}
组合式函数设计模式
什么是组合式函数
组合式函数(Composable Functions)是 Vue 3 Composition API 的核心概念之一。它们是一些可复用的逻辑单元,可以封装和重用组件中的业务逻辑。
// src/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 doubled = computed(() => count.value * 2)
return {
count,
increment,
decrement,
reset,
doubled
}
}
// src/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 () => {
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
}
}
watch(url, fetchData, { immediate: true })
return {
data,
loading,
error,
refetch: fetchData
}
}
组合式函数的实际应用
<template>
<div>
<h2>Counter Demo</h2>
<p>Count: {{ counter.count }}</p>
<p>Doubled: {{ counter.doubled }}</p>
<button @click="counter.increment">Increment</button>
<button @click="counter.decrement">Decrement</button>
<button @click="counter.reset">Reset</button>
<h2>Fetch Demo</h2>
<div v-if="fetcher.loading">Loading...</div>
<div v-else-if="fetcher.error">{{ fetcher.error }}</div>
<div v-else-if="fetcher.data">
<pre>{{ JSON.stringify(fetcher.data, null, 2) }}</pre>
</div>
<button @click="fetcher.refetch">Refresh</button>
</div>
</template>
<script>
import { useCounter } from '@/composables/useCounter'
import { useFetch } from '@/composables/useFetch'
export default {
setup() {
const counter = useCounter(10)
const fetcher = useFetch('https://jsonplaceholder.typicode.com/posts/1')
return {
counter,
fetcher
}
}
}
</script>
响应式数据管理
ref vs reactive 的深入对比
在 Vue 3 中,ref 和 reactive 是两种不同的响应式数据处理方式,它们各有适用场景。
import { ref, reactive, toRefs } from 'vue'
// 使用 ref
const count = ref(0)
const message = ref('Hello')
const user = ref({
name: 'John',
age: 30
})
// 访问值时需要使用 .value
console.log(count.value) // 0
count.value++ // 增加计数
// 使用 reactive
const state = reactive({
count: 0,
message: 'Hello',
user: {
name: 'John',
age: 30
}
})
// 直接访问属性,无需 .value
console.log(state.count) // 0
state.count++ // 增加计数
// 使用 toRefs 转换 reactive 对象
const state = reactive({
count: 0,
message: 'Hello'
})
const { count, message } = toRefs(state)
// 现在 count 和 message 都是 ref,可以像普通 ref 一样使用
复杂数据结构的响应式处理
import { ref, reactive, computed } from 'vue'
export default {
setup() {
// 响应式的数组
const items = ref([])
const addItem = (item) => {
items.value.push(item)
}
const removeItem = (index) => {
items.value.splice(index, 1)
}
// 响应式的对象集合
const users = reactive(new Map())
const addUser = (id, user) => {
users.set(id, user)
}
const removeUser = (id) => {
users.delete(id)
}
// 复杂嵌套结构的响应式处理
const formState = reactive({
personalInfo: {
name: '',
email: ''
},
address: {
street: '',
city: '',
country: ''
}
})
const validateForm = computed(() => {
return formState.personalInfo.name &&
formState.personalInfo.email &&
formState.address.street
})
// 处理深层嵌套的响应式数据
const deepState = reactive({
user: {
profile: {
settings: {
theme: 'light',
notifications: true
}
}
}
})
const updateTheme = (theme) => {
deepState.user.profile.settings.theme = theme
}
return {
items,
addItem,
removeItem,
users,
addUser,
removeUser,
formState,
validateForm,
updateTheme
}
}
}
响应式数据的性能优化
import { ref, reactive, computed, watchEffect, shallowRef, shallowReactive } from 'vue'
export default {
setup() {
// 使用 shallowRef 进行浅层响应式
const shallowCount = shallowRef(0)
// 使用 shallowReactive 进行浅层响应式对象
const shallowState = shallowReactive({
count: 0,
items: []
})
// watchEffect 的使用
const watchableData = ref(0)
watchEffect(() => {
console.log('watchEffect triggered:', watchableData.value)
// 这个副作用会自动追踪所有被访问的响应式数据
})
// 优化计算属性
const expensiveValue = computed({
get: () => {
// 复杂的计算逻辑
return Array.from({ length: 10000 }, (_, i) => i * 2).reduce((a, b) => a + b, 0)
},
set: (value) => {
// 可选的 setter
}
})
// 带有缓存的计算属性
const cachedComputed = computed(() => {
return expensiveValue.value * 2
})
// 监听器优化
const data = ref([])
// 使用 watch 的 immediate 和 flush 选项
watch(data, (newVal, oldVal) => {
console.log('Data changed:', newVal)
}, {
immediate: true, // 立即执行
flush: 'post' // 在 DOM 更新后执行
})
return {
shallowCount,
shallowState,
watchableData,
expensiveValue,
cachedComputed,
data
}
}
}
组件间通信策略
Props 和 Emit 的最佳实践
<template>
<div class="parent-component">
<h2>Parent Component</h2>
<child-component
:user="currentUser"
:items="listItems"
@update-user="handleUserUpdate"
@item-selected="handleItemSelected"
/>
<p>Current User: {{ currentUser.name }}</p>
<p>Selected Item: {{ selectedItem }}</p>
</div>
</template>
<script>
import { ref, reactive } from 'vue'
import ChildComponent from './ChildComponent.vue'
export default {
components: {
ChildComponent
},
setup() {
const currentUser = ref({
name: 'John Doe',
email: 'john@example.com'
})
const listItems = ref([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' }
])
const selectedItem = ref(null)
const handleUserUpdate = (updatedUser) => {
currentUser.value = updatedUser
}
const handleItemSelected = (item) => {
selectedItem.value = item
}
return {
currentUser,
listItems,
selectedItem,
handleUserUpdate,
handleItemSelected
}
}
}
</script>
<template>
<div class="child-component">
<h3>Child Component</h3>
<div class="user-info">
<p>Name: {{ user.name }}</p>
<p>Email: {{ user.email }}</p>
<button @click="updateUser">Update User</button>
</div>
<div class="items-list">
<h4>Items:</h4>
<ul>
<li
v-for="item in items"
:key="item.id"
@click="selectItem(item)"
>
{{ item.name }}
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
props: {
user: {
type: Object,
required: true
},
items: {
type: Array,
default: () => []
}
},
emits: ['updateUser', 'itemSelected'],
setup(props, { emit }) {
const updateUser = () => {
const updatedUser = {
...props.user,
name: props.user.name + ' (Updated)'
}
emit('updateUser', updatedUser)
}
const selectItem = (item) => {
emit('itemSelected', item)
}
return {
updateUser,
selectItem
}
}
}
</script>
Provide / Inject 的高级用法
// src/plugins/ThemeManager.js
import { ref, reactive } from 'vue'
export const useThemeManager = () => {
const theme = ref('light')
const colors = reactive({
primary: '#007bff',
secondary: '#6c757d',
success: '#28a745'
})
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
const setTheme = (newTheme) => {
theme.value = newTheme
}
return {
theme,
colors,
toggleTheme,
setTheme
}
}
<template>
<div class="app">
<header :class="`theme-${theme}`">
<h1>Theme Demo</h1>
<button @click="toggleTheme">Toggle Theme</button>
</header>
<main>
<slot />
</main>
</div>
</template>
<script>
import { useThemeManager } from '@/plugins/ThemeManager'
import { provide, computed } from 'vue'
export default {
setup() {
const { theme, toggleTheme, colors } = useThemeManager()
// 提供数据给子组件
provide('themeContext', {
theme,
colors,
toggleTheme
})
return {
theme,
toggleTheme
}
}
}
</script>
<template>
<div class="component-with-theme">
<h2>Component Using Theme</h2>
<p>Current theme: {{ themeContext.theme }}</p>
<button @click="changeColor">Change Color</button>
<div
class="color-box"
:style="{ backgroundColor: themeContext.colors.primary }"
>
Primary Color Box
</div>
</div>
</template>
<script>
import { inject, computed } from 'vue'
export default {
setup() {
const themeContext = inject('themeContext')
const changeColor = () => {
// 模拟颜色变化
themeContext.colors.primary = '#ff6b6b'
}
return {
themeContext,
changeColor
}
}
}
</script>
<style scoped>
.color-box {
width: 100px;
height: 100px;
margin: 20px 0;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
}
</style>
状态管理与全局状态
使用 Composition API 实现简单状态管理
// src/store/userStore.js
import { ref, reactive } from 'vue'
export const useUserStore = () => {
// 用户相关状态
const currentUser = ref(null)
const isLoggedIn = ref(false)
// 认证状态
const authStatus = reactive({
loading: false,
error: null
})
// 登录方法
const login = async (credentials) => {
try {
authStatus.loading = true
authStatus.error = null
// 模拟 API 调用
await new Promise(resolve => setTimeout(resolve, 1000))
currentUser.value = {
id: 1,
name: 'John Doe',
email: credentials.email
}
isLoggedIn.value = true
return { success: true }
} catch (error) {
authStatus.error = error.message
return { success: false, error }
} finally {
authStatus.loading = false
}
}
// 登出方法
const logout = () => {
currentUser.value = null
isLoggedIn.value = false
}
// 更新用户信息
const updateUser = (updates) => {
if (currentUser.value) {
Object.assign(currentUser.value, updates)
}
}
return {
currentUser,
isLoggedIn,
authStatus,
login,
logout,
updateUser
}
}
<template>
<div class="auth-demo">
<h2>Authentication Demo</h2>
<div v-if="!isLoggedIn">
<form @submit.prevent="handleLogin">
<input
v-model="loginForm.email"
type="email"
placeholder="Email"
required
/>
<input
v-model="loginForm.password"
type="password"
placeholder="Password"
required
/>
<button type="submit" :disabled="authStatus.loading">
{{ authStatus.loading ? 'Logging in...' : 'Login' }}
</button>
</form>
<div v-if="authStatus.error" class="error">
{{ authStatus.error }}
</div>
</div>
<div v-else>
<p>Welcome, {{ currentUser.name }}!</p>
<p>Email: {{ currentUser.email }}</p>
<button @click="handleLogout">Logout</button>
<button @click="updateProfile">
Update Profile
</button>
</div>
</div>
</template>
<script>
import { useUserStore } from '@/store/userStore'
import { ref } from 'vue'
export default {
setup() {
const {
currentUser,
isLoggedIn,
authStatus,
login,
logout,
updateUser
} = useUserStore()
const loginForm = ref({
email: '',
password: ''
})
const handleLogin = async () => {
const result = await login(loginForm.value)
if (result.success) {
loginForm.value = { email: '', password: '' }
}
}
const handleLogout = () => {
logout()
}
const updateProfile = () => {
updateUser({
name: 'John Smith'
})
}
return {
currentUser,
isLoggedIn,
authStatus,
loginForm,
handleLogin,
handleLogout,
updateProfile
}
}
}
</script>
复杂状态管理的组合式函数实现
// src/composables/useAsyncState.js
import { ref, computed } from 'vue'
export function useAsyncState(promiseFn, initialValue = null) {
const data = ref(initialValue)
const loading = ref(false)
const error = ref(null)
const executed = ref(false)
const execute = async (...args) => {
try {
loading.value = true
error.value = null
data.value = await promiseFn(...args)
executed.value = true
} catch (err) {
error.value = err
data.value = initialValue
} finally {
loading.value = false
}
}
const reset = () => {
data.value = initialValue
error.value = null
executed.value = false
}
return {
data,
loading,
error,
executed,
execute,
reset
}
}
// src/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 hasNextPage = computed(() => {
return currentPage.value < totalPages.value
})
const hasPrevPage = computed(() => {
return currentPage.value > 1
})
const nextPage = () => {
if (hasNextPage.value) {
currentPage.value++
}
}
const prevPage = () => {
if (hasPrevPage.value) {
currentPage.value--
}
}
const goToPage = (page) => {
if (page >= 1 && page <= totalPages.value) {
currentPage.value = page
}
}
const setPageSize = (size) => {
_pageSize.value = size
currentPage.value = 1
}
return {
currentPage,
totalPages,
paginatedData,
hasNextPage,
hasPrevPage,
nextPage,
prevPage,
goToPage,
setPageSize
}
}
<template>
<div class="advanced-demo">
<h2>Advanced State Management Demo</h2>
<!-- 异步状态管理 -->
<div class="async-section">
<h3>Async Data Fetching</h3>
<button @click="fetchPosts">Fetch Posts</button>
<div v-if="posts.loading">Loading...</div>
<div v-else-if="posts.error" class="error">
{{ posts.error }}
</div>
<div v-else-if="posts.data">
<ul>
<li v-for="post in posts.data" :key="post.id">
{{ post.title }}
</li>
</ul>
</div>
</div>
<!-- 分页管理 -->
<div class="pagination-section">
<h3>Pagination Demo</h3>
<div class="pagination-controls">
<button @click="goToPage(1)" :disabled="currentPage === 1">
First
</button>
<button @click="prevPage" :disabled="!hasPrevPage">
Previous
</button>
<span>Page {{ currentPage }} of {{ totalPages }}</span>
<button @click="nextPage" :disabled="!hasNextPage">
Next
</button>
<button @click="goToPage(totalPages)" :disabled="currentPage === totalPages">
Last
</button>
</div>
<div class="paginated-list">
<div
v-for="item in paginatedData"
:key="item.id"
class="list-item"
>
{{ item.name }}
</div>
</div>
</div>
</div>
</template>
<script>
import { useAsyncState } from '@/composables/useAsyncState'
import { usePagination } from '@/composables/usePagination'
import { ref, computed } from 'vue'
export default {
setup() {
// 异步状态管理
const fetchPosts = async () => {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=5')
const data = await response.json()
posts.data = data
} catch (error) {
posts.error = error.message
}
}
const posts = useAsyncState(fetchPosts, [])
// 分页管理
const items = ref([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' },
{ id: 4, name: 'Item 4' },
{ id: 5, name: 'Item 5' },
{ id: 6, name: 'Item 6' },
{ id: 7, name: 'Item 7' },
{ id: 8, name: 'Item 8' },
{ id: 9, name: 'Item 9' },
{ id: 10, name: 'Item 10' }
])
const {
currentPage,
totalPages,
paginatedData,
hasNextPage,
hasPrevPage,
nextPage,
prevPage,
goToPage
} = usePagination(items, 3)
return {
posts,
currentPage,
totalPages,
paginatedData,
hasNextPage,
hasPrevPage,
nextPage,
prevPage,
goToPage,
fetchPosts
}
}
}
</script>
<style scoped>
.async-section, .pagination-section {
margin: 20px 0;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
}
.pagination-controls {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 15px;
}
.list-item {
padding: 10px;
border-bottom: 1px solid #eee;
}
.error {
color: red;
font-weight: bold;
}
</style>
性能优化与最佳实践
组件复用的最佳实践
<template>
<div class="reusable-component">
<h2>{{ title }}</h2>
<div class="content">
<slot name="header"></slot>
<p>{{ content }}</p>
<slot></slot>
<slot name="footer"></slot>
</div>
</div>
</template>
<script>
export default {
props: {
title: {
type: String,
default: 'Default Title'
},
content: {
type: String,
default: ''
}
}
}
</script>
<style scoped>
.reusable-component {
border: 1px solid #ccc;
border-radius: 4px;
padding: 15px;
margin: 10px 0;
}
.content {
margin-top: 10px;
}
</style>
<template>
<div class="complex-demo">
<h2>Reusable Component Demo</h2>
<reusable-component
title="User Profile"
content="This is a user profile component"
>
<template #header>
<div class="profile-header">
<img :src="user.avatar" alt="Avatar" />
<h3>{{ user.name }}</h3>
</div>
</template>
<div class="profile-details">
<p>Email: {{ user.email }}</p>
<p>Role: {{ user.role }}</p>
</div>
<template #footer>
<button @click="editProfile">Edit Profile</button>
</template>
</reusable-component>
</div>
</template>
<script>
import ReusableComponent from './ReusableComponent.vue'
import { ref }
评论 (0)