Vue3组合式API
一、什么是组合式API
1. 基础概念
组合式 API (Composition API) 是一系列 API 的集合,使我们可以使用函数而不是声明选项的方式书写 Vue 组件。它是一个概括性的术语,涵盖了以下方面的 API:
- 响应式 API:例如
ref()和reactive(),使我们可以直接创建响应式状态、计算属性和侦听器。 - 生命周期钩子:例如
onMounted()和onUnmounted(),使我们可以在组件各个生命周期阶段添加逻辑。 - 依赖注入:例如
provide()和inject(),使我们可以在使用响应式 API 时,利用 Vue 的依赖注入系统。
组合式 API 是 Vue 3 及 Vue 2.7 的内置功能。对于更老的 Vue 2 版本,可以使用官方维护的插件 @vue/composition-api。在 Vue 3 中,组合式 API 基本上都会配合<script setup>语法在单文件组件中使用。
2. 为什么要有组合式API
- 更好的逻辑复用: 在选项式 API 中我们主要的逻辑复用机制是
mixins,而组合式 API 解决了 mixins 的所有缺陷。 - 更灵活的代码组织
- 更好的类型推导
- 更小的生产包体积
二、组件核心 API 的使用
1. 定义组件的 props
通过defineProps指定当前 props 类型,获得上下文的props对象。示例:
<script setup>
import { defineProps } from 'vue'
const props = defineProps({
title: String,
bar?: number
})
</script>
<!-- 或者 -->
<script setup lang="ts">
import { defineProps } from 'vue';
type Props = {
msg: string,
bar?: number
}
defineProps<Props>();
</script>2. 定义 emit
使用defineEmit定义当前组件含有的事件,并通过返回的上下文去执行emit。示例:
<script setup>
import { defineEmits } from 'vue'
const emit = defineEmits(['change', 'delete'])
</script>3. 父子组件通信
defineProps 用来接收父组件传来的 props ; defineEmits 用来声明触发的事件。
//父组件
<template>
<my-son foo="🚀🚀🚀🚀🚀🚀" @childClick="childClick" />
</template>
<script lang="ts" setup>
import MySon from "./MySon.vue";
let childClick = (e: any):void => {
console.log('from son:',e); //🚀🚀🚀🚀🚀🚀
};
</script>
//子组件
<template>
<span @click="sonToFather">信息:{{ props.foo }}</span>
</template>
<script lang="ts" setup>
import { defineEmits, defineProps} from "vue";
const emit = defineEmits(["childClick"]); // 声明触发事件 childClick
const props = defineProps({ foo: String }); // 获取props
const sonToFather = () =>{
emit('childClick' , props.foo)
}
</script>4. 定义响应变量、函数、监听、计算属性computed
<template>
<div>
<span>{{ count }}</span>
</div>
<div>
<span>{{ howCount }}</span>
</div>
</template>
<script setup lang="ts">
import { ref,computed,watchEffect } from 'vue';
const count = ref(0); /
const addCount = () => { //定义函数,使用同上
count.value++;
}
//定义计算属性
const howCount = computed(()=>"现在count值为:"+count.value);
//定义监听
watchEffect(() => console.log(count.value));
</script>- watchEffect: 用于有副作用的操作,会自动收集依赖。无需区分
deep,immediate,只要依赖的数据发生变化,就会调用。
5. reactive
此时name只会在初次创建的时候进行赋值,如果中间想要改变name的值,那么需要借助composition api 中的reactive。
<script setup lang="ts">
import { reactive, onUnmounted } from 'vue'
const state = reactive({
counter: 0
})
// 定时器 每秒都会更新数据
const timer = setInterval(() => {
state.counter++
}, 1000);
onUnmounted(() => {
clearInterval(timer);
})
</script>
<template>
<div>{{state.counter}}</div>
</template>使用ref也能达到我们预期的counter,并且在模板中,vue进行了处理,我们可以直接使用counter而不用写counter.value.
6. ref暴露变量到模板
<script setup lang="ts">
import { ref } from 'vue'
const counter = ref(0);//不用 return ,直接在 templete 中使用
const timer = setInterval(() => {
counter.value++
}, 1000)
onUnmounted(() => {
clearInterval(timer);
})
</script>
<template>
<div>{{counter}}</div>
</template>7. 生命周期方法
因为setup是围绕beforeCreate和created生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在setup函数中编写。
可以通过在生命周期钩子前面加上on来访问组件的生命周期钩子。下表包含如何在 setup () 内部调用生命周期钩子:
| 选项式 API | Hook inside setup |
|---|---|
| beforeCreate | Not needed* |
| created | Not needed* |
| beforeMount | onBeforeMount |
| mounted | onMounted |
| beforeUpdate | onBeforeUpdate |
| updated | onUpdated |
| beforeUnmount | onBeforeUnmount |
| unmounted | onUnmounted |
| errorCaptured | onErrorCaptured |
| renderTracked | onRenderTracked |
| renderTriggered | onRenderTriggered |
| activated | onActivated |
| deactivated | onDeactivated |
<script setup lang="ts">
import { onMounted } from 'vue';
onMounted(() => { console.log('mounted!'); });
</script>8. 获取 slots 和 attrs
通过useAttrs和useSlots获取 attrs 数据和插槽。
<template>
<component v-bind='attrs'></component>
</template>
<srcipt setup lang='ts'>
import { useAttrs, useSlots } from 'vue'
const attrs = useAttrs()
const slots = useSlots()
<script>9. defineExpose API
如果需要对外暴露 setup 中的数据和方法,需要使用 defineExpose API。示例:
//父组件
<template>
<Daughter ref="daughter" />
</template>
<script lang="ts" setup>
import { ref } from "vue";
import Daughter from "./Daughter.vue";
const daughter = ref(null)
console.log('🚀🚀🚀🚀~daughter', daughter)
</script>
//子组件
<template>
<div>妾身{{ msg }}</div>
</template>
<script lang="ts" setup>
import { ref, defineExpose} from "vue";
const msg = ref('貂蝉')
defineExpose({
msg
})
</script>10. 自定义Hook
自定义Hook是组合式API中逻辑复用的核心方式,它允许我们将相关的响应式状态和方法封装在一起。
<!-- useCounter.js -->
<script setup>
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 double = computed(() => count.value * 2)
return {
count,
increment,
decrement,
reset,
double
}
}
</script>
<!-- 在组件中使用 -->
<template>
<div>
<p>Count: {{ count }}</p>
<p>Double: {{ double }}</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
<button @click="reset">Reset</button>
</div>
</template>
<script setup>
import { useCounter } from './useCounter'
const { count, increment, decrement, reset, double } = useCounter(10)
</script>11. 响应式系统深入
<script setup>
import { ref, reactive, readonly, shallowRef, shallowReactive, toRef, toRefs, isRef, unref } from 'vue'
// ref vs reactive
const countRef = ref(0) // 基本类型用ref
const stateReactive = reactive({ // 对象用reactive
name: 'Vue3',
version: '3.0'
})
// readonly - 创建只读响应式对象
const readonlyState = readonly(stateReactive)
// shallowRef 和 shallowReactive - 浅层响应式
const shallowState = shallowReactive({
deep: {
nested: 'value'
}
})
const shallowCount = shallowRef({ count: 0 })
// toRef - 创建响应式对象的属性ref
const nameRef = toRef(stateReactive, 'name')
// toRefs - 解构响应式对象
const { name, version } = toRefs(stateReactive)
// 类型检查
console.log(isRef(countRef)) // true
console.log(isRef(stateReactive)) // false
// unref - 获取ref的值或原始值
const value = unref(countRef)
</script>12. 高级监听器
<script setup>
import { ref, watch, watchEffect, watchPostEffect, watchSyncEffect } from 'vue'
const count = ref(0)
const name = ref('Vue')
const user = ref({ age: 25 })
// watch - 监听特定数据源
watch(count, (newVal, oldVal) => {
console.log(`Count changed: ${oldVal} -> ${newVal}`)
})
// 监听多个数据源
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
console.log('Multiple values changed')
})
// 深度监听
watch(user, (newVal, oldVal) => {
console.log('User changed')
}, { deep: true })
// 立即执行
watch(count, (newVal) => {
console.log(`Count is now: ${newVal}`)
}, { immediate: true })
// watchEffect - 自动收集依赖
watchEffect(() => {
console.log(`Count is: ${count.value}`)
})
// watchPostEffect - 在DOM更新后执行
watchPostEffect(() => {
console.log('DOM updated')
})
// watchSyncEffect - 同步执行
watchSyncEffect(() => {
console.log('Sync effect')
})
</script>13. 依赖注入
<!-- 父组件 -->
<template>
<ChildComponent />
</template>
<script setup>
import { provide, ref, readonly } from 'vue'
import ChildComponent from './ChildComponent.vue'
// 提供响应式数据
const theme = ref('light')
const user = ref({ name: 'John', age: 30 })
provide('theme', readonly(theme)) // 提供只读的响应式数据
provide('user', user)
// 提供方法
provide('updateTheme', (newTheme) => {
theme.value = newTheme
})
</script>
<!-- 子组件 -->
<template>
<div :class="`theme-${theme}`">
<p>{{ user.name }} is {{ user.age }} years old</p>
<button @click="changeTheme">Change Theme</button>
</div>
</template>
<script setup>
import { inject } from 'vue'
const theme = inject('theme', 'dark') // 带默认值
const user = inject('user')
const updateTheme = inject('updateTheme')
const changeTheme = () => {
updateTheme(theme.value === 'light' ? 'dark' : 'light')
}
</script>14. 模板引用
<template>
<div ref="divRef">Hello</div>
<input ref="inputRef" type="text" />
<MyComponent ref="componentRef" />
</template>
<script setup>
import { ref, onMounted } from 'vue'
import MyComponent from './MyComponent.vue'
const divRef = ref(null)
const inputRef = ref(null)
const componentRef = ref(null)
onMounted(() => {
// 访问DOM元素
console.log(divRef.value) // div元素
console.log(inputRef.value) // input元素
// 访问组件实例
console.log(componentRef.value) // 组件实例
// 聚焦输入框
inputRef.value.focus()
// 调用组件方法
componentRef.value.someMethod()
})
</script>15. Teleport - 传送门
<!-- 模态框组件 -->
<template>
<teleport to="body">
<div v-if="isOpen" class="modal-overlay">
<div class="modal">
<h3>{{ title }}</h3>
<slot></slot>
<button @click="close">关闭</button>
</div>
</div>
</teleport>
</template>
<script setup>
import { ref, watch } from 'vue'
const props = defineProps({
isOpen: Boolean,
title: String
})
const emit = defineEmits(['close'])
const close = () => {
emit('close')
}
</script>
<style scoped>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
}
.modal {
background: white;
padding: 20px;
border-radius: 8px;
}
</style>16. Suspense - 异步组件
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
<script setup>
import { defineAsyncComponent } from 'vue'
const AsyncComponent = defineAsyncComponent(() => import('./AsyncComponent.vue'))
</script>
<!-- 异步组件 -->
<template>
<div>{{ message }}</div>
</template>
<script setup>
import { ref } from 'vue'
const message = ref('')
// 模拟异步操作
await new Promise(resolve => setTimeout(resolve, 2000))
message.value = 'Loaded successfully!'
</script>17. 组合式函数最佳实践
// composables/useFetch.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(false)
let controller = null
const fetchData = async () => {
if (controller) {
controller.abort()
}
controller = new AbortController()
loading.value = true
error.value = null
try {
const response = await fetch(url, {
signal: controller.signal
})
data.value = await response.json()
} catch (err) {
if (err.name !== 'AbortError') {
error.value = err
}
} finally {
loading.value = false
}
}
onMounted(fetchData)
onUnmounted(() => {
if (controller) {
controller.abort()
}
})
return {
data,
error,
loading,
refetch: fetchData
}
}18. 状态管理模式
// stores/useCounterStore.js
import { ref, computed, watch } from 'vue'
const globalState = ref({
count: 0,
name: 'Global Store'
})
export function useCounterStore() {
const count = computed(() => globalState.value.count)
const name = computed(() => globalState.value.name)
const increment = () => {
globalState.value.count++
}
const decrement = () => {
globalState.value.count--
}
const reset = () => {
globalState.value.count = 0
}
const setName = (newName) => {
globalState.value.name = newName
}
// 持久化到localStorage
watch(globalState, (newState) => {
localStorage.setItem('counterStore', JSON.stringify(newState))
}, { deep: true })
return {
count,
name,
increment,
decrement,
reset,
setName
}
}19. TypeScript集成
<script setup lang="ts">
import { ref, computed, type Ref } from 'vue'
// 定义接口
interface User {
id: number
name: string
age: number
}
interface Props {
title: string
user?: User
count: number
}
// 定义Props类型
const props = withDefaults(defineProps<Props>(), {
count: 0
})
// 定义Emits类型
const emit = defineEmits<{
update: [value: string]
change: [id: number, name: string]
}>()
// 响应式数据类型
const users: Ref<User[]> = ref([])
const currentUser = ref<User | null>(null)
// 计算属性类型
const adultUsers = computed<User[]>(() =>
users.value.filter(user => user.age >= 18)
)
// 函数类型
const updateUser = (id: number, updates: Partial<User>) => {
const index = users.value.findIndex(user => user.id === id)
if (index !== -1) {
users.value[index] = { ...users.value[index], ...updates }
}
}
// 使用
const handleClick = () => {
emit('update', 'new value')
emit('change', 1, 'John')
}
</script>20. 性能优化技巧
<script setup>
import { ref, shallowRef, markRaw, computed } from 'vue'
// 使用shallowRef优化大对象
const largeData = shallowRef({ /* 大量数据 */ })
// 使用markRaw避免响应式转换
const nonReactiveObject = markRaw({
// 不需要响应式的对象
})
// 计算属性缓存
const expensiveComputed = computed(() => {
// 昂贵的计算
return heavyCalculation()
})
// 避免不必要的响应式
const staticData = Object.freeze({
// 静态配置
})
// 使用v-once优化静态内容
</script>
<template>
<div v-once>
<!-- 静态内容只渲染一次 -->
<h1>{{ staticTitle }}</h1>
</div>
<!-- 动态内容 -->
<div>{{ dynamicContent }}</div>
</template>三、最佳实践与模式
1. 代码组织
// 按功能组织代码
// 1. 响应式数据
// 2. 计算属性
// 3. 方法
// 4. 生命周期钩子
// 5. 监听器
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
// 响应式数据
const state = reactive({
users: [],
loading: false,
error: null
})
// 计算属性
const filteredUsers = computed(() => {
return state.users.filter(user => user.active)
})
// 方法
const fetchUsers = async () => {
state.loading = true
try {
const response = await fetch('/api/users')
state.users = await response.json()
} catch (error) {
state.error = error
} finally {
state.loading = false
}
}
// 生命周期钩子
onMounted(() => {
fetchUsers()
})
// 监听器
watch(() => state.users, (newUsers) => {
console.log('Users updated:', newUsers.length)
})
</script>2. 错误处理
<script setup>
import { ref, onErrorCaptured } from 'vue'
const error = ref(null)
// 全局错误捕获
onErrorCaptured((err, instance, info) => {
error.value = err.message
console.error('Error captured:', err, info)
return false // 阻止错误继续向上传播
})
// 异步错误处理
const asyncOperation = async () => {
try {
const result = await someAsyncFunction()
return result
} catch (error) {
error.value = error.message
throw error
}
}
</script>
<template>
<div v-if="error" class="error">
错误: {{ error }}
</div>
<slot v-else />
</template>3. 组件设计原则
// 单一职责原则
// 组件只做一件事,做好一件事
// 纯展示组件
const PresentationComponent = {
props: ['data'],
setup(props) {
// 只负责展示,不修改数据
return { formattedData: formatData(props.data) }
}
}
// 容器组件
const ContainerComponent = {
setup() {
const data = ref([])
const loading = ref(false)
const fetchData = async () => {
loading.value = true
data.value = await api.getData()
loading.value = false
}
return { data, loading, fetchData }
}
}四、调试技巧
1. Vue DevTools
// 在组件中添加调试信息
<script setup>
import { ref, onMounted } from 'vue'
const debug = process.env.NODE_ENV === 'development'
const state = ref({
users: [],
loading: false
})
if (debug) {
// 开发环境下的调试代码
window.debugState = state
console.log('Debug mode enabled')
}
</script>2. 性能分析
<script setup>
import { onMounted } from 'vue'
onMounted(() => {
if (process.env.NODE_ENV === 'development') {
// 性能标记
performance.mark('component-mounted')
// 性能测量
setTimeout(() => {
performance.mark('component-ready')
performance.measure(
'component-load-time',
'component-mounted',
'component-ready'
)
const measure = performance.getEntriesByName('component-load-time')[0]
console.log(`Component load time: ${measure.duration}ms`)
}, 0)
}
})
</script>总结
Vue3的组合式API提供了更灵活、更强大的组件开发方式:
- 更好的逻辑复用:通过自定义Hook实现代码复用
- 更清晰的代码组织:相关逻辑可以组织在一起
- 更好的类型推导:原生TypeScript支持
- 更小的包体积:tree-shaking友好
- 更强的性能:响应式系统优化
掌握组合式API的关键是理解响应式原理和合理组织代码结构。在实际项目中,建议结合业务需求选择合适的模式,保持代码的可维护性和可扩展性。
