目录
前言
Vue3 的发布带来了全新的组合式 API(Composition API),它为我们提供了一种更灵活、更高效的组织组件逻辑的方式。相比于 Vue2 的选项式 API(Options API),组合式 API 允许我们按照逻辑关注点组织代码,而不是按照选项类型。本文将深入探讨组合式 API 的核心概念、实践技巧以及实战应用,帮助你快速掌握这一强大的特性。
组合式 API 基础
setup 函数
组合式 API 的核心是 setup
函数,它在组件创建之前执行,是组合式 API 的入口点。
import { ref, onMounted } from 'vue'
export default {
setup() {
// 声明响应式状态
const count = ref(0)
// 声明方法
function increment() {
count.value++
}
// 生命周期钩子
onMounted(() => {
console.log('组件已挂载')
})
// 返回值会暴露给模板和其他选项
return {
count,
increment
}
}
}
script setup 语法糖
Vue3.2 引入了 <script setup>
语法糖,它是组合式 API 的编译时语法糖,使代码更简洁:
<script setup>
import { ref, onMounted } from 'vue'
// 声明响应式状态
const count = ref(0)
// 声明方法
function increment() {
count.value++
}
// 生命周期钩子
onMounted(() => {
console.log('组件已挂载')
})
// 无需返回,所有顶层变量和导入都会自动暴露给模板
</script>
<template>
<button @click="increment">{{ count }}</button>
</template>
响应式系统
ref 和 reactive
Vue3 提供了两个主要的响应式 API:ref
和 reactive
。
ref
ref
用于创建任何类型的响应式数据,它将值包装在一个带有 .value
属性的对象中:
import { ref } from 'vue'
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
在模板中使用时,无需使用 .value
,Vue 会自动解包:
<template>
<div>{{ count }}</div>
</template>
reactive
reactive
用于创建响应式对象:
import { reactive } from 'vue'
const state = reactive({
count: 0,
name: 'Vue'
})
console.log(state.count) // 0
state.count++
计算属性
使用 computed
函数创建计算属性:
import { ref, computed } from 'vue'
const count = ref(0)
// 只读计算属性
const doubleCount = computed(() => count.value * 2)
// 可写计算属性
const fullName = computed({
get() {
return firstName.value + ' ' + lastName.value
},
set(newValue) {
[firstName.value, lastName.value] = newValue.split(' ')
}
})
监听器
使用 watch
和 watchEffect
监听响应式数据的变化:
import { ref, watch, watchEffect } from 'vue'
const count = ref(0)
// 监听单个数据源
watch(count, (newValue, oldValue) => {
console.log(`count 从 ${oldValue} 变为 ${newValue}`)
}, { deep: true, immediate: true })
// 监听多个数据源
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
console.log('数据变化了')
})
// 自动收集依赖并监听
watchEffect(() => {
console.log(`count 的当前值是: ${count.value}`)
})
生命周期钩子
组合式 API 提供了一系列生命周期钩子函数,它们与选项式 API 中的钩子对应:
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
onErrorCaptured,
onActivated,
onDeactivated
} from 'vue'
export default {
setup() {
onBeforeMount(() => {
console.log('组件挂载前')
})
onMounted(() => {
console.log('组件已挂载')
})
onBeforeUpdate(() => {
console.log('组件更新前')
})
onUpdated(() => {
console.log('组件已更新')
})
onBeforeUnmount(() => {
console.log('组件卸载前')
})
onUnmounted(() => {
console.log('组件已卸载')
})
// 其他钩子...
}
}
依赖注入
使用 provide
和 inject
实现跨组件通信:
// 父组件
import { provide, ref } from 'vue'
export default {
setup() {
const theme = ref('light')
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
// 提供值和方法
provide('theme', theme)
provide('toggleTheme', toggleTheme)
}
}
// 子组件或后代组件
import { inject } from 'vue'
export default {
setup() {
// 注入值和方法
const theme = inject('theme')
const toggleTheme = inject('toggleTheme')
return {
theme,
toggleTheme
}
}
}
组合式函数(Composables)
组合式函数是组合式 API 最强大的特性之一,它允许我们将逻辑封装并在多个组件之间复用:
// useCounter.js
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
function increment() {
count.value++
}
function decrement() {
count.value--
}
const doubleCount = computed(() => count.value * 2)
return {
count,
increment,
decrement,
doubleCount
}
}
// 在组件中使用
import { useCounter } from './composables/useCounter'
export default {
setup() {
const { count, increment, decrement, doubleCount } = useCounter(10)
return {
count,
increment,
decrement,
doubleCount
}
}
}
常见的组合式函数示例
1. 使用 fetch 数据
// useFetch.js
import { ref, computed, watchEffect } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(true)
const fetchData = async () => {
loading.value = true
try {
const response = await fetch(url)
data.value = await response.json()
} catch (err) {
error.value = err
} finally {
loading.value = false
}
}
watchEffect(() => {
fetchData()
})
return { data, error, loading, refetch: fetchData }
}
// 使用
const { data, error, loading } = useFetch('https://api.example.com/data')
2. 管理本地存储
// useLocalStorage.js
import { ref, watch } from 'vue'
export function useLocalStorage(key, defaultValue) {
const storedValue = localStorage.getItem(key)
const value = ref(storedValue ? JSON.parse(storedValue) : defaultValue)
watch(value, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue))
}, { deep: true })
return value
}
// 使用
const user = useLocalStorage('user', { name: '', age: 0 })
实战案例
待办事项应用
下面是一个使用组合式 API 构建的简单待办事项应用:
<script setup>
import { ref, computed, watch } from 'vue'
// 从本地存储加载待办事项
const loadTodos = () => {
const saved = localStorage.getItem('todos')
return saved ? JSON.parse(saved) : []
}
const todos = ref(loadTodos())
const newTodo = ref('')
const filter = ref('all') // all, active, completed
// 保存到本地存储
watch(todos, (newTodos) => {
localStorage.setItem('todos', JSON.stringify(newTodos))
}, { deep: true })
// 计算属性:过滤后的待办事项
const filteredTodos = computed(() => {
switch (filter.value) {
case 'active':
return todos.value.filter(todo => !todo.completed)
case 'completed':
return todos.value.filter(todo => todo.completed)
default:
return todos.value
}
})
// 计算属性:剩余未完成项
const remaining = computed(() => {
return todos.value.filter(todo => !todo.completed).length
})
// 添加新待办事项
function addTodo() {
if (newTodo.value.trim()) {
todos.value.push({
id: Date.now(),
text: newTodo.value.trim(),
completed: false
})
newTodo.value = ''
}
}
// 移除待办事项
function removeTodo(todo) {
const index = todos.value.findIndex(t => t.id === todo.id)
if (index !== -1) {
todos.value.splice(index, 1)
}
}
// 清除已完成项
function clearCompleted() {
todos.value = todos.value.filter(todo => !todo.completed)
}
</script>
<template>
<div class="todo-app">
<h1>待办事项</h1>
<div class="add-todo">
<input
v-model="newTodo"
@keyup.enter="addTodo"
placeholder="添加新待办事项..."
/>
<button @click="addTodo">添加</button>
</div>
<div class="filters">
<button
:class="{ active: filter === 'all' }"
@click="filter = 'all'"
>
全部
</button>
<button
:class="{ active: filter === 'active' }"
@click="filter = 'active'"
>
未完成
</button>
<button
:class="{ active: filter === 'completed' }"
@click="filter = 'completed'"
>
已完成
</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)">删除</button>
</li>
</ul>
<div class="todo-footer" v-if="todos.length > 0">
<span>{{ remaining }} 项未完成</span>
<button @click="clearCompleted" v-if="remaining < todos.length">
清除已完成
</button>
</div>
</div>
</template>
<style scoped>
.todo-app {
max-width: 500px;
margin: 0 auto;
padding: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.add-todo {
display: flex;
margin-bottom: 20px;
}
.add-todo input {
flex-grow: 1;
padding: 8px;
border: 1px solid #ddd;
}
button {
padding: 8px 12px;
background-color: #4CAF50;
color: white;
border: none;
cursor: pointer;
margin-left: 5px;
}
.filters {
display: flex;
margin-bottom: 15px;
}
.filters button {
background-color: #f1f1f1;
color: #333;
}
.filters button.active {
background-color: #4CAF50;
color: white;
}
.todo-list {
list-style: none;
padding: 0;
}
.todo-list li {
display: flex;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #eee;
}
.todo-list li.completed span {
text-decoration: line-through;
color: #888;
}
.todo-list span {
flex-grow: 1;
margin: 0 10px;
}
.todo-footer {
display: flex;
justify-content: space-between;
margin-top: 15px;
color: #666;
}
</style>
性能优化
1. 使用 shallowRef 和 shallowReactive
当处理大型对象或不需要深层响应性时,使用浅层响应式 API 可以提高性能:
import { shallowRef, shallowReactive } from 'vue'
// 只有 state.count 是响应式的,state.nested 的变化不会触发更新
const state = shallowReactive({
count: 0,
nested: { value: 'hello' }
})
// 只有 .value 的替换是响应式的,.value.nested 的变化不会触发更新
const data = shallowRef({ nested: { value: 'hello' } })
2. 使用 v-once 和 v-memo
对于不需要频繁更新的内容,可以使用 v-once
和 v-memo
指令:
<template>
<!-- 只渲染一次,后续更新会被跳过 -->
<div v-once>{{ expensiveComputation() }}</div>
<!-- 只有当 id 变化时才会重新渲染 -->
<div v-memo="[item.id]">
<span>{{ item.name }}</span>
<span>{{ item.description }}</span>
</div>
</template>
3. 使用 defineAsyncComponent 懒加载组件
import { defineAsyncComponent } from 'vue'
const AsyncComponent = defineAsyncComponent(() =>
import('./components/HeavyComponent.vue')
)
与 TypeScript 结合
Vue3 提供了出色的 TypeScript 支持,以下是一些使用 TypeScript 的最佳实践:
定义 Props 类型
<script setup lang="ts">
import { defineProps } from 'vue'
// 方式一:使用运行时声明
const props = defineProps({
title: { type: String, required: true },
count: { type: Number, default: 0 }
})
// 方式二:使用类型声明(推荐)
interface Props {
title: string
count?: number
}
const props = defineProps<Props>()
// 方式三:使用类型声明并提供默认值
withDefaults(defineProps<Props>(), {
count: 0
})
</script>
定义 Emits 类型
<script setup lang="ts">
// 方式一:运行时声明
const emit = defineEmits(['update', 'delete'])
// 方式二:类型声明(推荐)
const emit = defineEmits<{
(e: 'update', id: number, value: string): void
(e: 'delete', id: number): void
}>()
// 使用
function handleUpdate(id: number) {
emit('update', id, 'new value')
}
</script>
为 ref 和 reactive 添加类型
import { ref, reactive } from 'vue'
// 为 ref 添加类型
const count = ref<number>(0)
const user = ref<{ name: string; age: number } | null>(null)
// 为 reactive 添加类型
interface User {
name: string
age: number
}
const state = reactive<User>({
name: 'John',
age: 30
})
迁移策略
从 Vue2 迁移到 Vue3 的组合式 API 可以采取渐进式策略:
1. 混合使用选项式 API 和组合式 API
Vue3 允许在同一个组件中混合使用两种 API:
export default {
data() {
return {
// 选项式 API 数据
message: 'Hello'
}
},
setup() {
// 组合式 API 逻辑
const count = ref(0)
return {
count
}
},
methods: {
// 选项式 API 方法
greet() {
console.log(this.message)
console.log(this.count) // 可以访问 setup 返回的属性
}
}
}
2. 逐步重构
- 首先识别组件中的逻辑关注点
- 将每个关注点提取为组合式函数
- 在
setup
中使用这些组合式函数 - 逐步移除选项式 API 代码
3. 使用 @vue/composition-api 在 Vue2 中提前体验
对于 Vue2 项目,可以使用 @vue/composition-api
插件提前体验组合式 API:
// main.js
import Vue from 'vue'
import VueCompositionAPI from '@vue/composition-api'
Vue.use(VueCompositionAPI)
总结
Vue3 的组合式 API 为我们提供了一种全新的组织组件逻辑的方式,它具有以下优势:
- 逻辑复用:通过组合式函数(Composables)可以轻松实现逻辑复用
- 更好的类型推导:与 TypeScript 结合提供了出色的类型支持
- 更好的代码组织:按照逻辑关注点组织代码,而不是选项类型
- 更小的打包体积:支持更好的 Tree-shaking
- 更灵活的逻辑组合:可以根据需要组合各种功能
组合式 API 并不是要完全取代选项式 API,而是提供了另一种组织组件代码的方式。对于简单组件,选项式 API 仍然是一个很好的选择;而对于复杂组件或需要逻辑复用的场景,组合式 API 则能发挥其优势。
通过本指南的学习,相信你已经掌握了 Vue3 组合式 API 的核心概念和实践技巧。接下来,就是在实际项目中不断实践和探索,充分发挥组合式 API 的强大能力!
参考资料: