深入理解Vue 3的Composition API,掌握响应式系统、生命周期钩子、依赖注入等核心概念
前言
Vue 3引入的Composition API是一个革命性的特性,它改变了我们组织组件逻辑的方式。相比于Options API,Composition API提供了更灵活、更易维护的代码组织方式。
本文将详细介绍Composition API的核心概念和实战技巧。
什么是Composition API?
Composition API是一组基于函数的API,允许我们使用导入的函数来组合组件逻辑,而不是依赖选项式API来组织代码。
Options API vs Composition API
Options API:
export default {
data() {
return {
count: 0,
message: 'Hello'
}
},
computed: {
doubled() {
return this.count * 2
}
},
methods: {
increment() {
this.count++
}
},
mounted() {
console.log('Component mounted')
}
}
Composition API:
import { ref, computed, onMounted } from 'vue'
export default {
setup() {
const count = ref(0)
const message = ref('Hello')
const doubled = computed(() => count.value * 2)
const increment = () => {
count.value++
}
onMounted(() => {
console.log('Component mounted')
})
return {
count,
message,
doubled,
increment
}
}
}
Composition API的优势
- 更好的代码组织:按功能而非选项类型组织代码
- 逻辑复用:可以轻松提取和复用逻辑
- 类型推断:更好的TypeScript支持
- 更灵活:不局限于组件内部
核心概念
1. 响应式系统
Vue 3提供了两种创建响应式状态的方式:ref和reactive。
ref
ref用于创建包装后的响应式对象,适合基础类型:
import { ref } from 'vue'
const count = ref(0)
const message = ref('Hello')
const isActive = ref(false)
// 访问值
console.log(count.value) // 0
// 修改值
count.value = 1
使用场景:
- 数字、字符串、布尔值等基础类型
- 需要整个替换的对象
// 组件中使用
export default {
setup() {
const count = ref(0)
const increment = () => {
count.value++
}
return {
count,
increment
}
}
}
reactive
reactive用于创建响应式对象,适合对象和数组:
import { reactive } from 'vue'
const state = reactive({
name: 'Vue',
version: '3.0',
features: ['Composition API', 'Teleport', 'Fragments']
})
// 访问属性
console.log(state.name) // 'Vue'
// 修改属性
state.version = '3.1'
// 添加新属性
state.author = 'Evan You'
使用场景:
- 对象类型的数据
- 需要深层响应式的复杂数据
// 组件中使用
export default {
setup() {
const article = reactive({
title: '',
content: '',
tags: []
})
const addTag = (tag: string) => {
article.tags.push(tag)
}
return {
article,
addTag
}
}
}
toRef和toRefs
当使用reactive时,需要使用toRef和toRefs来保持响应性:
import { reactive, toRef, toRefs } from 'vue'
const state = reactive({
name: 'Vue',
version: '3.0'
})
// toRef - 转换单个属性
const version = toRef(state, 'version')
console.log(version.value) // '3.0'
// toRefs - 转换整个对象
export default {
setup() {
const state = reactive({
name: 'Vue',
version: '3.0'
})
// 展开所有属性,保持响应性
return {
...toRefs(state)
}
}
}
2. 计算属性
使用computed创建计算属性:
import { ref, computed } from 'vue'
const count = ref(0)
const doubled = computed(() => count.value * 2)
// 只读
console.log(doubled.value) // 0
count.value = 5
console.log(doubled.value) // 10
可写的计算属性:
const count = ref(0)
const doubled = computed({
get: () => count.value * 2,
set: (value) => {
count.value = value / 2
}
})
doubled.value = 10
console.log(count.value) // 5
实战案例:文章列表过滤
import { ref, computed } from 'vue'
export default {
setup() {
const articles = ref([
{ id: 1, title: 'Vue 3教程', category: 'Vue' },
{ id: 2, title: 'React入门', category: 'React' },
{ id: 3, title: 'TypeScript指南', category: 'TS' }
])
const selectedCategory = ref('')
const filteredArticles = computed(() => {
if (!selectedCategory.value) {
return articles.value
}
return articles.value.filter(
a => a.category === selectedCategory.value
)
})
return {
articles,
selectedCategory,
filteredArticles
}
}
}
3. 侦听器
使用watch和watchEffect侦听数据变化。
watch
侦听特定数据源的变化:
import { ref, watch } from 'vue'
const count = ref(0)
watch(count, (newValue, oldValue) => {
console.log(`count changed from ${oldValue} to ${newValue}`)
})
count.value = 1 // 输出: count changed from 0 to 1
侦听多个数据源:
const firstName = ref('John')
const lastName = ref('Doe')
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
console.log(`Name changed from ${oldFirst} ${oldLast} to ${newFirst} ${newLast}`)
})
侦听对象属性:
const user = reactive({
name: 'John',
age: 30
})
watch(() => user.name, (newName, oldName) => {
console.log(`Name changed from ${oldName} to ${newName}`)
})
配置选项:
watch(
source,
callback,
{
immediate: true, // 立即执行一次
deep: true, // 深度侦听
flush: 'post' // 回调时机:pre/post/sync
}
)
watchEffect
自动追踪依赖,无需显式指定:
import { ref, watchEffect } from 'vue'
const count = ref(0)
const doubled = computed(() => count.value * 2)
watchEffect(() => {
console.log(`count is ${count.value}, doubled is ${doubled.value}`)
})
count.value = 1 // 自动重新执行
停止侦听:
const stop = watchEffect(() => {
console.log('effect runs')
})
// 停止侦听
stop()
实战案例:草稿自动保存
import { ref, watchEffect } from 'vue'
export function useAutoSave(formData: any) {
const isSaving = ref(false)
const lastSaved = ref<number | null>(null)
let timer: any = null
watchEffect(() => {
// 清除之前的定时器
clearTimeout(timer)
// 5秒后保存
timer = setTimeout(() => {
saveDraft(formData)
isSaving.value = false
lastSaved.value = Date.now()
}, 5000)
isSaving.value = true
})
return {
isSaving,
lastSaved
}
}
4. 生命周期钩子
在Composition API中,生命周期钩子需要在setup函数中调用:
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
onErrorCaptured
} from 'vue'
export default {
setup() {
onBeforeMount(() => {
console.log('组件挂载前')
})
onMounted(() => {
console.log('组件已挂载')
})
onBeforeUpdate(() => {
console.log('组件更新前')
})
onUpdated(() => {
console.log('组件已更新')
})
onBeforeUnmount(() => {
console.log('组件卸载前')
})
onUnmounted(() => {
console.log('组件已卸载')
})
onErrorCaptured((err) => {
console.error('捕获错误:', err)
return false // 阻止错误继续向上传播
})
}
}
生命周期映射:
| Options API | Composition API |
|---|---|
| beforeCreate | setup() |
| created | setup() |
| beforeMount | onBeforeMount |
| mounted | onMounted |
| beforeUpdate | onBeforeUpdate |
| updated | onUpdated |
| beforeUnmount | onBeforeUnmount |
| unmounted | onUnmounted |
5. 依赖注入
使用provide和inject在组件树中传递数据:
import { provide, inject } from 'vue'
// 父组件
export default {
setup() {
const theme = ref('dark')
const user = reactive({
name: 'John',
role: 'admin'
})
// 提供数据
provide('theme', theme)
provide('user', user)
// 提供可修改的方法
provide('toggleTheme', () => {
theme.value = theme.value === 'dark' ? 'light' : 'dark'
})
}
}
// 子组件
export default {
setup() {
// 注入数据
const theme = inject('theme')
const user = inject('user')
const toggleTheme = inject('toggleTheme')
return {
theme,
user,
toggleTheme
}
}
}
默认值:
const theme = inject('theme', 'light')
实战案例:主题切换
// composables/useTheme.ts
import { provide, inject, ref, readonly } from 'vue'
const ThemeKey = Symbol('theme')
export function provideTheme() {
const isDark = ref(false)
const toggle = () => {
isDark.value = !isDark.value
document.documentElement.classList.toggle('dark')
}
const setDark = (dark: boolean) => {
isDark.value = dark
if (dark) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
}
provide(ThemeKey, {
isDark: readonly(isDark),
toggle,
setDark
})
}
export function useTheme() {
const theme = inject(ThemeKey)
if (!theme) {
throw new Error('Theme not provided')
}
return theme
}
Composables:逻辑复用
Composables是Composition API的核心优势之一,它让我们能够提取和复用逻辑。
创建Composable
// composables/useCounter.ts
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const doubled = computed(() => count.value * 2)
const increment = () => {
count.value++
}
const decrement = () => {
count.value--
}
const reset = () => {
count.value = initialValue
}
return {
count,
doubled,
increment,
decrement,
reset
}
}
使用Composable
import { useCounter } from './composables/useCounter'
export default {
setup() {
const {
count,
doubled,
increment,
decrement,
reset
} = useCounter(10)
return {
count,
doubled,
increment,
decrement,
reset
}
}
}
实战案例:文章管理
// composables/useArticle.ts
import { ref, computed, onMounted } from 'vue'
export function useArticle() {
const articles = ref<Article[]>([])
const loading = ref(false)
const error = ref<Error | null>(null)
const selectedCategory = ref('')
const filteredArticles = computed(() => {
if (!selectedCategory.value) {
return articles.value
}
return articles.value.filter(
a => a.category === selectedCategory.value
)
})
const hotArticles = computed(() => {
return articles.value
.filter(a => a.views > 1000)
.sort((a, b) => b.views - a.views)
})
const fetchArticles = async () => {
loading.value = true
error.value = null
try {
articles.value = await getArticles()
} catch (e) {
error.value = e as Error
} finally {
loading.value = false
}
}
const addArticle = (article: Article) => {
articles.value.unshift(article)
}
const updateArticle = (id: number, updates: Partial<Article>) => {
const index = articles.value.findIndex(a => a.id === id)
if (index !== -1) {
articles.value[index] = {
...articles.value[index],
...updates
}
}
}
const deleteArticle = (id: number) => {
articles.value = articles.value.filter(a => a.id !== id)
}
onMounted(() => {
fetchArticles()
})
return {
articles,
loading,
error,
selectedCategory,
filteredArticles,
hotArticles,
fetchArticles,
addArticle,
updateArticle,
deleteArticle
}
}
最佳实践
1. 按功能组织代码
export default {
setup() {
// 文章相关逻辑
const { articles, fetchArticles } = useArticles()
// 用户相关逻辑
const { user, login, logout } = useAuth()
// 主题相关逻辑
const { theme, toggleTheme } = useTheme()
return {
articles,
user,
theme,
fetchArticles,
login,
logout,
toggleTheme
}
}
}
2. 使用TypeScript
import { ref, Ref } from 'vue'
interface Article {
id: number
title: string
content: string
views: number
}
export function useArticleList(): {
articles: Ref<Article[]>
fetchArticles: () => Promise<void>
} {
const articles = ref<Article[]>([])
const fetchArticles = async () => {
articles.value = await getArticles()
}
return {
articles,
fetchArticles
}
}
3. 避免过度使用ref
// ❌ 不推荐:过多ref
const title = ref('')
const content = ref('')
const category = ref('')
const tags = ref([])
const isPublished = ref(false)
// ✅ 推荐:使用reactive
const article = reactive({
title: '',
content: '',
category: '',
tags: [],
isPublished: false
})
4. 合理使用computed
// ❌ 不推荐:在模板中直接计算
// <div>{{ article.content.length }}</div>
// ✅ 推荐:使用computed
const contentLength = computed(() => article.content.length)
// <div>{{ contentLength }}</div>
总结
Composition API提供了更灵活、更强大的代码组织方式:
- 逻辑复用:通过Composables轻松复用逻辑
- 代码组织:按功能而非选项类型组织代码
- 类型安全:更好的TypeScript支持
- 灵活性:不局限于组件内部
掌握Composition API,能让你写出更优雅、更易维护的Vue 3代码!
标签:#Vue3 #CompositionAPI #前端 #JavaScript #TypeScript
点赞❤️ + 收藏⭐️ + 评论💬,你的支持是我创作的动力!