Vue3的Composition API彻底改变了Vue的开发方式,本文将深入剖析组合式API的核心概念和最佳实践,帮助你快速掌握Vue3开发。
一、为什么需要Composition API?
1.1 Options API的痛点
痛点表现:
- 逻辑分散:相关代码被data、methods、computed等选项分割
- 代码复用困难:mixins容易命名冲突,来源不清晰
- 类型推导弱:TypeScript支持不够友好
- 大组件难维护:代码量大时难以理解和维护
javascript
// ❌ Options API:逻辑分散
export default {
data() {
return {
count: 0,
user: null,
loading: false
}
},
computed: {
doubleCount() {
return this.count * 2
}
},
methods: {
increment() {
this.count++
},
async fetchUser() {
this.loading = true
this.user = await api.getUser()
this.loading = false
}
},
mounted() {
this.fetchUser()
}
}
1.2 Composition API的优势
- ✅ 逻辑聚合:相关代码组织在一起
- ✅ 代码复用:通过组合函数轻松复用
- ✅ 类型推导:完美支持TypeScript
- ✅ Tree-shaking:未使用的代码可以被移除
二、核心API详解
2.1 ref 和 reactive
关键点: ref用于基本类型,reactive用于对象类型。
vue
<template>
<div>
<!-- ref需要.value访问,模板中自动解包 -->
<p>计数: {{ count }}</p>
<p>双倍: {{ doubleCount }}</p>
<!-- reactive对象直接访问属性 -->
<p>用户: {{ user.name }}</p>
<p>年龄: {{ user.age }}</p>
<button @click="increment">增加</button>
<button @click="updateUser">更新用户</button>
</div>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
// ✅ ref:基本类型响应式
const count = ref(0)
const message = ref('Hello')
// 在JS中需要.value访问
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
// ✅ reactive:对象类型响应式
const user = reactive({
name: '张三',
age: 25,
address: {
city: '北京'
}
})
// 直接访问属性
console.log(user.name) // 张三
user.age = 26
// computed计算属性
const doubleCount = computed(() => count.value * 2)
// 方法
const increment = () => {
count.value++
}
const updateUser = () => {
user.name = '李四'
user.age = 30
}
// ❌ 常见错误:解构reactive会失去响应式
const { name, age } = user // 失去响应式!
// ✅ 正确:使用toRefs保持响应式
import { toRefs } from 'vue'
const { name, age } = toRefs(user) // 保持响应式
</script>
痛点解决: 清晰的响应式数据管理,避免this指向问题。
2.2 生命周期钩子
关键点: 组合式API中的生命周期钩子以on开头。
vue
<script setup>
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
onErrorCaptured
} from 'vue'
// 组件挂载前
onBeforeMount(() => {
console.log('组件即将挂载')
})
// 组件挂载后(最常用)
onMounted(() => {
console.log('组件已挂载')
// 适合:DOM操作、发起请求、初始化第三方库
fetchData()
initChart()
})
// 组件更新前
onBeforeUpdate(() => {
console.log('组件即将更新')
})
// 组件更新后
onUpdated(() => {
console.log('组件已更新')
// 注意:避免在这里修改状态,可能导致无限循环
})
// 组件卸载前
onBeforeUnmount(() => {
console.log('组件即将卸载')
// 适合:清理定时器、取消请求、移除事件监听
})
// 组件卸载后
onUnmounted(() => {
console.log('组件已卸载')
})
// 错误捕获
onErrorCaptured((err, instance, info) => {
console.error('捕获到错误:', err)
return false // 阻止错误继续传播
})
// ✅ 可以多次调用同一个钩子
onMounted(() => {
console.log('第一个mounted')
})
onMounted(() => {
console.log('第二个mounted')
})
</script>
对比Options API:
| Options API | Composition API |
|---|---|
| beforeCreate | setup() |
| created | setup() |
| beforeMount | onBeforeMount |
| mounted | onMounted |
| beforeUpdate | onBeforeUpdate |
| updated | onUpdated |
| beforeUnmount | onBeforeUnmount |
| unmounted | onUnmounted |
2.3 watch 和 watchEffect
关键点: watch需要明确指定监听源,watchEffect自动追踪依赖。
vue
<script setup>
import { ref, reactive, watch, watchEffect } from 'vue'
const count = ref(0)
const user = reactive({ name: '张三', age: 25 })
// ✅ watch:监听单个ref
watch(count, (newVal, oldVal) => {
console.log(`count从${oldVal}变为${newVal}`)
})
// ✅ watch:监听多个源
watch([count, () => user.age], ([newCount, newAge], [oldCount, oldAge]) => {
console.log('count或age变化了')
})
// ✅ watch:监听reactive对象(深度监听)
watch(user, (newVal, oldVal) => {
console.log('user对象变化了')
// 注意:newVal和oldVal是同一个对象引用
}, { deep: true })
// ✅ watch:监听reactive对象的某个属性
watch(() => user.name, (newName, oldName) => {
console.log(`name从${oldName}变为${newName}`)
})
// ✅ watchEffect:自动追踪依赖
watchEffect(() => {
// 自动追踪count和user.age的变化
console.log(`count: ${count.value}, age: ${user.age}`)
})
// watch选项
watch(count, (newVal) => {
console.log(newVal)
}, {
immediate: true, // 立即执行一次
deep: true, // 深度监听
flush: 'post' // 在DOM更新后执行
})
// 停止监听
const stop = watch(count, (newVal) => {
console.log(newVal)
if (newVal > 10) {
stop() // 停止监听
}
})
// ❌ 常见错误:监听reactive对象的属性
watch(user.name, (newVal) => { // 错误!
console.log(newVal)
})
// ✅ 正确写法
watch(() => user.name, (newVal) => {
console.log(newVal)
})
</script>
痛点解决: 灵活的数据监听,避免不必要的性能开销。
2.4 computed 计算属性
关键点: computed具有缓存特性,只有依赖变化时才重新计算。
vue
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
// ✅ 只读计算属性
const fullName = computed(() => {
console.log('计算fullName') // 只在依赖变化时执行
return `${firstName.value} ${lastName.value}`
})
// ✅ 可写计算属性
const fullNameWritable = computed({
get() {
return `${firstName.value} ${lastName.value}`
},
set(value) {
const names = value.split(' ')
firstName.value = names[0]
lastName.value = names[1]
}
})
// 使用
console.log(fullName.value) // 张 三
fullNameWritable.value = '李 四' // 触发set
console.log(firstName.value) // 李
console.log(lastName.value) // 四
// ❌ 常见错误:在computed中修改依赖
const badComputed = computed(() => {
firstName.value = '王' // 错误!不要在computed中修改依赖
return firstName.value
})
// ✅ computed vs method
const list = ref([1, 2, 3, 4, 5])
// computed:有缓存,依赖不变不重新计算
const filteredList = computed(() => {
console.log('计算filteredList')
return list.value.filter(n => n > 2)
})
// method:无缓存,每次调用都执行
const getFilteredList = () => {
console.log('执行getFilteredList')
return list.value.filter(n => n > 2)
}
</script>
<template>
<div>
<!-- computed:多次访问只计算一次 -->
<p>{{ filteredList }}</p>
<p>{{ filteredList }}</p>
<!-- method:每次都执行 -->
<p>{{ getFilteredList() }}</p>
<p>{{ getFilteredList() }}</p>
</div>
</template>
痛点解决: 自动缓存计算结果,避免重复计算,提升性能。
三、组合函数(Composables)
3.1 基础组合函数
关键点: 将可复用的逻辑提取为组合函数,实现代码复用。
javascript
// composables/useCounter.js
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const doubleCount = computed(() => count.value * 2)
const increment = () => {
count.value++
}
const decrement = () => {
count.value--
}
const reset = () => {
count.value = initialValue
}
return {
count,
doubleCount,
increment,
decrement,
reset
}
}
vue
<!-- 使用组合函数 -->
<template>
<div>
<p>计数: {{ count }}</p>
<p>双倍: {{ doubleCount }}</p>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
<button @click="reset">重置</button>
</div>
</template>
<script setup>
import { useCounter } from '@/composables/useCounter'
// 可以创建多个独立的计数器
const { count, doubleCount, increment, decrement, reset } = useCounter(10)
const counter2 = useCounter(0)
</script>
3.2 实战:鼠标位置追踪
javascript
// composables/useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
const update = (event) => {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
return { x, y }
}
vue
<template>
<div>
<p>鼠标位置: {{ x }}, {{ y }}</p>
</div>
</template>
<script setup>
import { useMouse } from '@/composables/useMouse'
const { x, y } = useMouse()
</script>
3.3 实战:异步数据获取
javascript
// composables/useFetch.js
import { ref, watchEffect, toValue } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(false)
const fetchData = async () => {
loading.value = true
error.value = null
data.value = null
try {
// toValue可以处理ref和普通值
const urlValue = toValue(url)
const response = await fetch(urlValue)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
data.value = await response.json()
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
}
// 当url变化时重新获取
watchEffect(() => {
fetchData()
})
const refetch = () => {
fetchData()
}
return { data, error, loading, refetch }
}
vue
<template>
<div>
<div v-if="loading">加载中...</div>
<div v-else-if="error">错误: {{ error }}</div>
<div v-else-if="data">
<h3>{{ data.title }}</h3>
<p>{{ data.body }}</p>
</div>
<button @click="refetch">刷新</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useFetch } from '@/composables/useFetch'
const userId = ref(1)
const url = computed(() => `https://jsonplaceholder.typicode.com/posts/${userId.value}`)
const { data, error, loading, refetch } = useFetch(url)
</script>
3.4 实战:本地存储
javascript
// composables/useLocalStorage.js
import { ref, watch } from 'vue'
export function useLocalStorage(key, defaultValue) {
// 从localStorage读取初始值
const storedValue = localStorage.getItem(key)
const data = ref(storedValue ? JSON.parse(storedValue) : defaultValue)
// 监听变化并同步到localStorage
watch(data, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue))
}, { deep: true })
// 清除
const remove = () => {
localStorage.removeItem(key)
data.value = defaultValue
}
return { data, remove }
}
vue
<template>
<div>
<input v-model="username.data" placeholder="输入用户名">
<p>保存的用户名: {{ username.data }}</p>
<button @click="username.remove">清除</button>
</div>
</template>
<script setup>
import { useLocalStorage } from '@/composables/useLocalStorage'
const username = useLocalStorage('username', '')
</script>
痛点解决: 逻辑复用变得简单,代码更加模块化和可维护。
四、高级技巧
4.1 provide / inject 依赖注入
关键点: 跨层级组件通信,避免props层层传递。
vue
<!-- 父组件 -->
<template>
<div>
<ChildComponent />
</div>
</template>
<script setup>
import { provide, ref, readonly } from 'vue'
import ChildComponent from './ChildComponent.vue'
const theme = ref('dark')
const user = ref({ name: '张三', role: 'admin' })
// 提供数据
provide('theme', theme)
provide('user', readonly(user)) // 只读,防止子组件修改
// 提供方法
const updateTheme = (newTheme) => {
theme.value = newTheme
}
provide('updateTheme', updateTheme)
</script>
vue
<!-- 子组件(任意层级) -->
<template>
<div :class="theme">
<p>当前主题: {{ theme }}</p>
<p>用户: {{ user.name }}</p>
<button @click="updateTheme('light')">切换主题</button>
</div>
</template>
<script setup>
import { inject } from 'vue'
// 注入数据
const theme = inject('theme')
const user = inject('user')
const updateTheme = inject('updateTheme')
// 提供默认值
const config = inject('config', { timeout: 3000 })
</script>
4.2 defineExpose 暴露组件方法
关键点: <script setup>默认是封闭的,需要显式暴露给父组件。
vue
<!-- 子组件 -->
<template>
<div>
<p>{{ message }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
const message = ref('Hello')
const count = ref(0)
const updateMessage = (newMessage) => {
message.value = newMessage
}
const increment = () => {
count.value++
}
// 暴露给父组件
defineExpose({
message,
count,
updateMessage,
increment
})
</script>
vue
<!-- 父组件 -->
<template>
<div>
<ChildComponent ref="childRef" />
<button @click="callChildMethod">调用子组件方法</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
const childRef = ref(null)
const callChildMethod = () => {
// 访问子组件暴露的属性和方法
console.log(childRef.value.message)
childRef.value.updateMessage('New Message')
childRef.value.increment()
}
</script>
4.3 自定义指令
javascript
// directives/vFocus.js
export const vFocus = {
mounted(el) {
el.focus()
}
}
// directives/vClickOutside.js
export const vClickOutside = {
mounted(el, binding) {
el.clickOutsideEvent = (event) => {
if (!(el === event.target || el.contains(event.target))) {
binding.value(event)
}
}
document.addEventListener('click', el.clickOutsideEvent)
},
unmounted(el) {
document.removeEventListener('click', el.clickOutsideEvent)
}
}
vue
<template>
<div>
<!-- 自动聚焦 -->
<input v-focus placeholder="自动聚焦">
<!-- 点击外部关闭 -->
<div v-click-outside="closeDropdown" class="dropdown">
<button @click="showDropdown = !showDropdown">下拉菜单</button>
<ul v-if="showDropdown">
<li>选项1</li>
<li>选项2</li>
</ul>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { vFocus, vClickOutside } from '@/directives'
const showDropdown = ref(false)
const closeDropdown = () => {
showDropdown.value = false
}
</script>
五、实战案例:Todo应用
vue
<template>
<div class="todo-app">
<h1>待办事项</h1>
<!-- 输入框 -->
<div class="input-box">
<input
v-model="newTodo"
@keyup.enter="addTodo"
placeholder="添加新任务..."
>
<button @click="addTodo">添加</button>
</div>
<!-- 过滤器 -->
<div class="filters">
<button
v-for="filter in filters"
:key="filter"
:class="{ active: currentFilter === filter }"
@click="currentFilter = filter"
>
{{ filter }}
</button>
</div>
<!-- 任务列表 -->
<ul class="todo-list">
<li
v-for="todo in filteredTodos"
:key="todo.id"
:class="{ completed: todo.completed }"
>
<input
type="checkbox"
v-model="todo.completed"
>
<span>{{ todo.text }}</span>
<button @click="removeTodo(todo.id)">删除</button>
</li>
</ul>
<!-- 统计 -->
<div class="stats">
<span>总计: {{ todos.length }}</span>
<span>已完成: {{ completedCount }}</span>
<span>未完成: {{ activeCount }}</span>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useLocalStorage } from '@/composables/useLocalStorage'
// 使用本地存储
const { data: todos } = useLocalStorage('todos', [])
const newTodo = ref('')
const currentFilter = ref('全部')
const filters = ['全部', '未完成', '已完成']
// 添加任务
const addTodo = () => {
if (!newTodo.value.trim()) return
todos.value.push({
id: Date.now(),
text: newTodo.value,
completed: false
})
newTodo.value = ''
}
// 删除任务
const removeTodo = (id) => {
const index = todos.value.findIndex(todo => todo.id === id)
if (index > -1) {
todos.value.splice(index, 1)
}
}
// 过滤任务
const filteredTodos = computed(() => {
switch (currentFilter.value) {
case '未完成':
return todos.value.filter(todo => !todo.completed)
case '已完成':
return todos.value.filter(todo => todo.completed)
default:
return todos.value
}
})
// 统计
const completedCount = computed(() =>
todos.value.filter(todo => todo.completed).length
)
const activeCount = computed(() =>
todos.value.filter(todo => !todo.completed).length
)
</script>
<style scoped>
.todo-app {
max-width: 600px;
margin: 50px auto;
padding: 20px;
}
.input-box {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.input-box input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
.filters {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.filters button {
padding: 8px 16px;
border: 1px solid #ddd;
background: white;
cursor: pointer;
border-radius: 4px;
}
.filters button.active {
background: #42b983;
color: white;
border-color: #42b983;
}
.todo-list {
list-style: none;
padding: 0;
}
.todo-list li {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
border-bottom: 1px solid #eee;
}
.todo-list li.completed span {
text-decoration: line-through;
color: #999;
}
.stats {
display: flex;
justify-content: space-around;
margin-top: 20px;
padding: 10px;
background: #f5f5f5;
border-radius: 4px;
}
</style>
六、最佳实践
✅ 命名规范
- 组合函数以
use开头:useCounter、useMouse - 事件处理函数以
handle或on开头:handleClick、onSubmit - 布尔值以
is、has、should开头:isLoading、hasError
✅ 代码组织
javascript
// 推荐的代码组织顺序
<script setup>
// 1. 导入
import { ref, computed, watch } from 'vue'
import { useRouter } from 'vue-router'
// 2. 组合函数
const router = useRouter()
const { data, loading } = useFetch('/api/data')
// 3. 响应式数据
const count = ref(0)
const user = reactive({ name: '' })
// 4. 计算属性
const doubleCount = computed(() => count.value * 2)
// 5. 监听器
watch(count, (newVal) => {
console.log(newVal)
})
// 6. 生命周期
onMounted(() => {
fetchData()
})
// 7. 方法
const increment = () => {
count.value++
}
// 8. 暴露
defineExpose({ increment })
</script>
✅ 性能优化
- 使用
shallowRef和shallowReactive处理大对象 - 使用
markRaw标记不需要响应式的对象 - 合理使用
computed缓存计算结果 - 避免在模板中使用复杂表达式
七、总结
Vue3 Composition API的核心优势:
- 逻辑复用 - 通过组合函数轻松复用逻辑
- 代码组织 - 相关代码聚合在一起,易于维护
- 类型推导 - 完美支持TypeScript
- 性能优化 - 更好的Tree-shaking支持
记住:Composition API不是替代Options API,而是提供了更灵活的选择。
相关资源
💡 小贴士: 从小项目开始尝试Composition API,逐步掌握其精髓!
关注我,获取更多Vue干货! 🚀