文章目录
正文
1. Vue组合式API介绍
组合式API(Composition API)是Vue 3引入的一种新的编写组件逻辑的方式,它是对Vue 2中选项式API(Options API)的补充和扩展。组合式API允许开发者按照逻辑关注点组织代码,提高代码的可读性和可维护性。
1.1 组合式API的核心优势
- 更好的逻辑复用: 通过可复用的组合函数替代混入(mixins)
- 更灵活的代码组织: 按功能/逻辑关注点组织代码
- 更好的类型推断: 为TypeScript提供更自然的支持
- 更小的包体积: 通过摇树优化(tree-shaking)减小打包体积
1.2 基础概念对比
选项式API (Options API) | 组合式API (Composition API) |
---|---|
data() | ref(), reactive() |
methods | 普通函数 |
computed | computed() |
watch | watch(), watchEffect() |
生命周期钩子 | onMounted(), onUpdated()等 |
2. 组合式API核心函数
2.1 setup函数
setup
是组合式API的入口点,它在组件实例创建之前执行,此时组件实例尚未被创建。
<script>
export default {
setup(props, context) {
// 这里编写使用组合式API的代码
// setup函数需要返回一个对象,对象中的属性会暴露给模板使用
return {
// 要暴露给模板的属性和方法
}
}
}
</script>
2.2 ref和reactive
2.2.1 ref
ref
用于创建一个响应式的数据引用,接受一个任意值作为参数,返回一个带有.value
属性的响应式对象。
import { ref } from 'vue'
const count = ref(0)
console.log(count.value) // 0
// 更新值
count.value++
console.log(count.value) // 1
在模板中使用时,可以直接使用ref名称,Vue会自动解包.value
:
<template>
<div>{{ count }}</div>
</template>
2.2.2 reactive
reactive
用于创建一个响应式对象:
import { reactive } from 'vue'
const state = reactive({
count: 0,
name: 'Vue',
users: ['Alice', 'Bob']
})
// 直接修改属性
state.count++
state.name = 'Vue 3'
state.users.push('Charlie')
2.3 computed
computed
用于创建计算属性:
import { ref, computed } from 'vue'
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
console.log(doubleCount.value) // 0
count.value = 2
console.log(doubleCount.value) // 4
也可以创建可写的计算属性:
const count = ref(0)
const doubleCount = computed({
get: () => count.value * 2,
set: (val) => {
count.value = val / 2
}
})
doubleCount.value = 10
console.log(count.value) // 5
3. 生命周期钩子
组合式API提供了一系列生命周期钩子函数,它们可以在setup
函数中使用:
import {
onMounted,
onUpdated,
onUnmounted,
onBeforeMount,
onBeforeUpdate,
onBeforeUnmount
} from 'vue'
export default {
setup() {
onBeforeMount(() => {
console.log('组件挂载前')
})
onMounted(() => {
console.log('组件已挂载')
})
onBeforeUpdate(() => {
console.log('组件更新前')
})
onUpdated(() => {
console.log('组件已更新')
})
onBeforeUnmount(() => {
console.log('组件卸载前')
})
onUnmounted(() => {
console.log('组件已卸载')
})
}
}
3.1 生命周期对比表
选项式API | 组合式API |
---|---|
beforeCreate | setup() |
created | setup() |
beforeMount | onBeforeMount() |
mounted | onMounted() |
beforeUpdate | onBeforeUpdate() |
updated | onUpdated() |
beforeUnmount | onBeforeUnmount() |
unmounted | onUnmounted() |
errorCaptured | onErrorCaptured() |
renderTracked | onRenderTracked() |
renderTriggered | onRenderTriggered() |
activated | onActivated() |
deactivated | onDeactivated() |
4. watch和watchEffect
4.1 watch
watch
用于监听特定的响应式引用或源,并在其变化时执行回调函数:
import { ref, watch } from 'vue'
const count = ref(0)
// 监听单个ref
watch(count, (newValue, oldValue) => {
console.log(`count从${oldValue}变为${newValue}`)
})
// 监听多个源
const name = ref('Vue')
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
console.log(`count: ${oldCount} -> ${newCount}, name: ${oldName} -> ${newName}`)
})
// 监听对象的深层属性
const user = reactive({
name: 'Alice',
age: 25
})
watch(() => user.name, (newName, oldName) => {
console.log(`name从${oldName}变为${newName}`)
})
// 深度监听
watch(user, (newUser, oldUser) => {
console.log('user对象发生变化')
}, { deep: true })
4.2 watchEffect
watchEffect
会立即执行一次回调函数,并自动追踪其中的响应式依赖,当这些依赖变化时重新执行回调:
import { ref, watchEffect } from 'vue'
const count = ref(0)
const name = ref('Vue')
watchEffect(() => {
console.log(`count: ${count.value}, name: ${name.value}`)
})
// 立即输出: "count: 0, name: Vue"
// 修改值后会触发watchEffect重新执行
count.value++
// 输出: "count: 1, name: Vue"
4.3 清理副作用
watch
和watchEffect
都支持在回调函数中返回一个清理函数,用于在下次执行前清理副作用:
watchEffect((onCleanup) => {
const timer = setTimeout(() => {
console.log('执行某些操作')
}, 1000)
// 在watchEffect重新执行前或停止时调用
onCleanup(() => {
clearTimeout(timer)
})
})
5. 组合函数(Composables)
组合函数是组合式API最强大的特性之一,它允许你封装和复用有状态的逻辑:
5.1 创建组合函数
组合函数通常以use
开头,返回一个包含所需状态和方法的对象:
// useCounter.js
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const doubled = computed(() => count.value * 2)
function increment() {
count.value++
}
function decrement() {
count.value--
}
return {
count,
doubled,
increment,
decrement
}
}
5.2 使用组合函数
在组件中使用组合函数:
<template>
<div>
<p>Count: {{ count }}</p>
<p>Doubled: {{ doubled }}</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
</div>
</template>
<script>
import { useCounter } from './useCounter'
export default {
setup() {
// 解构并使用组合函数返回的内容
const { count, doubled, increment, decrement } = useCounter(10)
return {
count,
doubled,
increment,
decrement
}
}
}
</script>
6. script setup语法糖
Vue 3.2引入了<script setup>
语法糖,进一步简化组合式API的使用:
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
// 在<script setup>中的变量和函数会自动暴露给模板
const count = ref(0)
function increment() {
count.value++
}
</script>
6.1 特点和优势
- 更简洁的语法,无需返回对象
- 自动将变量暴露给模板
- 更好的IDE支持和类型推断
- 组件和指令可以直接使用,无需注册
6.2 defineProps和defineEmits
在<script setup>
中使用props和emits:
<script setup>
// 声明props
const props = defineProps({
message: String,
count: {
type: Number,
default: 0
}
})
// 声明emits
const emit = defineEmits(['increment', 'update'])
function handleClick() {
emit('increment', 1)
}
</script>
6.3 defineExpose
默认情况下,<script setup>
组件是关闭的(父组件无法访问子组件的属性和方法)。使用defineExpose
显式暴露内部属性:
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() { count.value++ }
// 暴露给父组件
defineExpose({
count,
increment
})
</script>
7. 组合式API实战案例
7.1 实现一个Todo List
<template>
<div class="todo-app">
<h1>Todo List</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">
<input
type="checkbox"
:checked="todo.completed"
@change="toggleTodo(todo.id)"
/>
<span :class="{ completed: todo.completed }">{{ todo.text }}</span>
<button @click="removeTodo(todo.id)">删除</button>
</li>
</ul>
<div class="todo-stats" v-if="todos.length > 0">
<span>{{ activeCount }} 个待办项</span>
<button v-if="completedCount > 0" @click="clearCompleted">
清除已完成
</button>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
// 状态
const newTodo = ref('')
const todos = ref([
{ id: 1, text: '学习Vue 3', completed: false },
{ id: 2, text: '掌握组合式API', completed: false },
{ id: 3, text: '构建项目', completed: false }
])
const filter = ref('all')
let nextId = 4
// 计算属性
const filteredTodos = computed(() => {
if (filter.value === 'active') {
return todos.value.filter(todo => !todo.completed)
} else if (filter.value === 'completed') {
return todos.value.filter(todo => todo.completed)
} else {
return todos.value
}
})
const activeCount = computed(() => {
return todos.value.filter(todo => !todo.completed).length
})
const completedCount = computed(() => {
return todos.value.filter(todo => todo.completed).length
})
// 方法
function addTodo() {
const text = newTodo.value.trim()
if (text) {
todos.value.push({
id: nextId++,
text,
completed: false
})
newTodo.value = ''
}
}
function toggleTodo(id) {
const todo = todos.value.find(todo => todo.id === id)
if (todo) {
todo.completed = !todo.completed
}
}
function removeTodo(id) {
const index = todos.value.findIndex(todo => todo.id === id)
if (index !== -1) {
todos.value.splice(index, 1)
}
}
function clearCompleted() {
todos.value = todos.value.filter(todo => !todo.completed)
}
</script>
<style scoped>
.todo-app {
max-width: 500px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}
h1 {
text-align: center;
}
.add-todo {
display: flex;
margin-bottom: 15px;
}
.add-todo input {
flex-grow: 1;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px 0 0 4px;
}
.add-todo button {
padding: 8px 15px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 0 4px 4px 0;
cursor: pointer;
}
.filters {
display: flex;
justify-content: center;
margin-bottom: 15px;
}
.filters button {
margin: 0 5px;
padding: 5px 10px;
border: 1px solid #ddd;
background-color: white;
cursor: pointer;
border-radius: 4px;
}
.filters button.active {
background-color: #ddd;
}
.todo-list {
list-style-type: none;
padding: 0;
}
.todo-list li {
display: flex;
align-items: center;
padding: 10px;
border-bottom: 1px solid #eee;
}
.todo-list span {
flex-grow: 1;
margin: 0 10px;
}
.todo-list span.completed {
text-decoration: line-through;
color: #aaa;
}
.todo-list button {
padding: 5px 8px;
background-color: #f44336;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.todo-stats {
display: flex;
justify-content: space-between;
margin-top: 15px;
color: #666;
}
.todo-stats button {
padding: 5px 10px;
background-color: #ddd;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
7.2 封装一个useAxios组合函数
// useAxios.js
import { ref, watch } from 'vue'
import axios from 'axios'
export function useAxios(url, options = {}) {
const data = ref(null)
const loading = ref(false)
const error = ref(null)
const response = ref(null)
// 可配置参数
const config = {
immediate: true, // 是否立即执行请求
...options
}
// 执行请求的函数
const execute = async (customUrl = url, customOptions = {}) => {
loading.value = true
error.value = null
data.value = null
try {
const result = await axios({
url: customUrl,
...config,
...customOptions
})
response.value = result
data.value = result.data
return result
} catch (err) {
error.value = err
return Promise.reject(err)
} finally {
loading.value = false
}
}
// 如果设置了immediate,则立即执行请求
if (config.immediate && url) {
execute()
}
// 取消请求
const cancel = () => {
if (axios.isCancel) {
// 实现取消请求的逻辑
}
}
// 刷新请求
const refresh = () => {
return execute()
}
return {
data,
loading,
error,
response,
execute,
cancel,
refresh
}
}
// 使用示例
/*
import { useAxios } from './useAxios'
export default {
setup() {
const {
data,
loading,
error,
execute
} = useAxios('https://jsonplaceholder.typicode.com/posts')
return {
data,
loading,
error,
fetchData: execute
}
}
}
*/
8. 组合式API与TypeScript
组合式API为TypeScript提供了出色的类型支持:
8.1 基本类型定义
import { ref, reactive, computed } from 'vue'
// 为ref指定类型
const count = ref<number>(0)
// 为reactive指定类型
interface User {
name: string
age: number
}
const user = reactive<User>({
name: 'Vue',
age: 3
})
// 为computed指定类型
const doubleCount = computed<number>(() => count.value * 2)
8.2 组件Props类型定义
<script setup lang="ts">
// 使用接口定义Props
interface Props {
message: string
count?: number
items: string[]
callback?: (id: number) => void
}
// 声明具有类型的props
const props = defineProps<Props>()
// 为props提供默认值
const defaults = withDefaults(defineProps<Props>(), {
count: 0,
items: () => ['默认项']
})
</script>
8.3 事件类型定义
<script setup lang="ts">
// 定义发出的事件类型
const emit = defineEmits<{
(e: 'update', id: number): void
(e: 'delete', id: number): void
}>()
// 使用事件
function handleUpdate(id: number) {
emit('update', id)
}
</script>
9. 性能优化最佳实践
9.1 使用shallowRef和shallowReactive
当处理大型数据结构且只需要跟踪顶层属性的变化时,使用浅层响应式可以提高性能:
import { shallowRef, shallowReactive } from 'vue'
// 只有.value改变时才会触发更新,不会深度追踪对象内部属性
const state = shallowRef({ count: 0, deep: { nested: 'value' } })
// 只跟踪顶层属性的变化
const user = shallowReactive({
name: 'Vue',
profile: { age: 3 }
})
9.2 使用toRaw获取原始对象
当需要处理大量数据但不需要触发响应式更新时,可以使用toRaw
获取原始对象:
import { reactive, toRaw } from 'vue'
const state = reactive({ count: 0, items: [] })
// 获取原始对象,对它的修改不会触发视图更新
const rawState = toRaw(state)
9.3 使用markRaw标记永不需要转换的对象
对于那些永远不需要转换为响应式的对象,使用markRaw
可以避免不必要的性能开销:
import { reactive, markRaw } from 'vue'
// 不会被转换为响应式
const data = markRaw({
large: 'data structure',
with: 'many properties'
})
const state = reactive({
title: 'My App',
// data将保持非响应式
data
})
10. 组合式API常见问题与解决方案
10.1 响应式丢失问题
问题:解构reactive对象会导致响应式丢失
const state = reactive({ count: 0 })
// 错误用法:响应式丢失
const { count } = state
解决方法:
// 方法1:使用toRefs保持响应性
import { toRefs } from 'vue'
const { count } = toRefs(state)
// 方法2:使用计算属性
import { computed } from 'vue'
const count = computed({
get: () => state.count,
set: (val) => state.count = val
})
// 方法3:直接使用state.count
10.2 ref的自动解包规则
在组合式API中,ref的自动解包有一些特殊情况需要注意:
- 在模板中会自动解包
- 作为reactive对象的属性会自动解包
- 在数组或普通对象中不会自动解包
const count = ref(0)
const state = reactive({
count, // 在reactive中会自动解包
users: [ref('Alice')] // 在数组中不会自动解包
})
console.log(state.count) // 直接访问值,不需要.value
console.log(state.users[0].value) // 需要使用.value
10.3 避免过度使用watchEffect
watchEffect
虽然强大,但可能会导致不必要的重复执行。应该谨慎使用,并考虑以下优化:
// 优化前
watchEffect(() => {
console.log(`name: ${user.name}, age: ${user.age}`)
// 只关心name的变化,但age变化也会触发
})
// 优化后
watch(() => user.name, (newName) => {
console.log(`name: ${newName}, age: ${user.age}`)
}, { immediate: true })
11. 高级用法
11.1 provide和inject
组合式API中的依赖注入:
// 父组件
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)
return { theme, toggleTheme }
}
}
// 子/孙组件
import { inject } from 'vue'
export default {
setup() {
// 注入父/祖先组件提供的值
const theme = inject('theme')
const toggleTheme = inject('toggleTheme')
return { theme, toggleTheme }
}
}
11.2 自定义ref
创建自定义的响应式引用:
import { customRef } from 'vue'
// 创建一个防抖ref
function useDebouncedRef(value, delay = 200) {
let timeout
return customRef((track, trigger) => {
return {
get() {
track() // 追踪依赖
return value
},
set(newValue) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
trigger() // 触发更新
}, delay)
}
}
})
}
// 使用
const searchQuery = useDebouncedRef('', 500)
11.3 Suspense和异步组件
组合式API中处理异步组件:
<!-- 父组件使用Suspense -->
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
<!-- 异步组件 -->
<script setup>
import { ref } from 'vue'
// setup可以是异步的
const data = await fetch('/api/data').then(r => r.json())
const processedData = ref(data)
</script>
12. 从选项式API迁移到组合式API
12.1 渐进式迁移策略
- 不需要一次性全部迁移
- 可以在同一个组件中混合使用两种API
<script>
import { ref, computed, onMounted } from 'vue'
export default {
// 选项式API
props: ['title'],
components: { /* ... */ },
// 组合式API
setup(props) {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
onMounted(() => {
console.log('组件已挂载')
})
return {
count,
doubleCount
}
},
// 依然可以使用选项式API的选项
data() {
return {
message: 'Hello'
}
},
methods: {
// ...
}
}
</script>
12.2 常见模式转换
12.2.1 data → ref/reactive
// 选项式API
data() {
return {
count: 0,
user: { name: 'Vue', age: 3 }
}
}
// 组合式API
setup() {
const count = ref(0)
const user = reactive({ name: 'Vue', age: 3 })
return { count, user }
}
12.2.2 methods → 普通函数
// 选项式API
methods: {
increment() {
this.count++
}
}
// 组合式API
setup() {
const count = ref(0)
function increment() {
count.value++
}
return { count, increment }
}
12.2.3 computed → computed()
// 选项式API
computed: {
doubleCount() {
return this.count * 2
}
}
// 组合式API
setup() {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
return { count, doubleCount }
}
13. 结合第三方库
13.1 Vue Router与组合式API
使用组合式API的Vue Router钩子:
import { useRouter, useRoute } from 'vue-router'
export default {
setup() {
const router = useRouter()
const route = useRoute()
// 使用路由参数
console.log(route.params.id)
// 导航
function navigateTo(id) {
router.push(`/user/${id}`)
}
return { navigateTo }
}
}
结语
感谢您的阅读!期待您的一键三连!欢迎指正!