Composition API 深度解析
Composition API 是 Vue 3 最重要的特性,解决了 Options API 在大型组件中逻辑分散的问题,让逻辑复用变得优雅。
Options API vs Composition API
vue
<!-- Options API:同一功能的代码分散在各个选项中 -->
<script>
export default {
data() {
return { count: 0, user: null, loading: false }
},
computed: {
doubled() { return this.count * 2 }
},
methods: {
increment() { this.count++ },
async fetchUser() {
this.loading = true
this.user = await getUser()
this.loading = false
}
},
mounted() {
this.fetchUser()
}
}
</script>
<!-- Composition API:按功能组织,逻辑内聚 -->
<script setup>
import { ref, computed, onMounted } from 'vue'
// 计数器逻辑
const count = ref(0)
const doubled = computed(() => count.value * 2)
const increment = () => count.value++
// 用户数据逻辑
const user = ref(null)
const loading = ref(false)
async function fetchUser() {
loading.value = true
user.value = await getUser()
loading.value = false
}
onMounted(fetchUser)
</script><script setup> 语法
vue
<script setup lang="ts">
import { ref, computed } from 'vue'
// 顶层声明自动暴露给模板
const count = ref(0)
const message = computed(() => `Count: ${count.value}`)
// Props 定义
const props = defineProps<{
title: string
items: string[]
modelValue?: number
}>()
// Props 默认值(withDefaults)
const props2 = withDefaults(defineProps<{
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
}>(), {
size: 'md',
disabled: false
})
// Emits 定义
const emit = defineEmits<{
'update:modelValue': [value: number]
'change': [value: string, index: number]
}>()
// v-model 支持
function handleInput(e: Event) {
emit('update:modelValue', +(e.target as HTMLInputElement).value)
}
// 暴露给父组件(通过 ref)
defineExpose({ count, reset: () => { count.value = 0 } })
</script>生命周期
ts
import {
onBeforeMount, onMounted,
onBeforeUpdate, onUpdated,
onBeforeUnmount, onUnmounted,
onErrorCaptured, onActivated, onDeactivated
} from 'vue'
// 对应关系
// Options API → Composition API
// beforeCreate → setup() 本身
// created → setup() 本身
// beforeMount → onBeforeMount
// mounted → onMounted
// beforeUpdate → onBeforeUpdate
// updated → onUpdated
// beforeUnmount → onBeforeUnmount
// unmounted → onUnmounted
onMounted(() => {
console.log('组件已挂载,可以访问 DOM')
})
onUnmounted(() => {
console.log('清理副作用:取消订阅、清除定时器等')
})
// 错误边界
onErrorCaptured((err, instance, info) => {
console.error('捕获到子组件错误:', err)
return false // 阻止错误继续向上传播
})组合式函数(Composables)
ts
// useCounter.ts
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const doubled = computed(() => count.value * 2)
const isEven = computed(() => count.value % 2 === 0)
function increment(step = 1) { count.value += step }
function decrement(step = 1) { count.value -= step }
function reset() { count.value = initialValue }
return { count, doubled, isEven, increment, decrement, reset }
}
// useFetch.ts
import { ref, watchEffect, toValue, type MaybeRefOrGetter } from 'vue'
export function useFetch<T>(url: MaybeRefOrGetter<string>) {
const data = ref<T | null>(null)
const error = ref<Error | null>(null)
const loading = ref(false)
watchEffect(async () => {
const resolvedUrl = toValue(url) // 支持 ref、getter 或普通值
if (!resolvedUrl) return
loading.value = true
error.value = null
try {
const res = await fetch(resolvedUrl)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
data.value = await res.json()
} catch (e) {
error.value = e as Error
} finally {
loading.value = false
}
})
return { data, error, loading }
}
// 使用
const userId = ref(1)
const { data: user, loading } = useFetch<User>(
() => `/api/users/${userId.value}` // 响应式 URL
)provide / inject
ts
// 父组件提供
import { provide, ref } from 'vue'
const theme = ref('light')
provide('theme', theme) // 提供响应式值
// 类型安全的 provide/inject(推荐)
import { InjectionKey } from 'vue'
interface ThemeContext {
theme: Ref<string>
toggle: () => void
}
const ThemeKey: InjectionKey<ThemeContext> = Symbol('theme')
provide(ThemeKey, {
theme,
toggle: () => { theme.value = theme.value === 'light' ? 'dark' : 'light' }
})
// 子孙组件注入
import { inject } from 'vue'
const themeCtx = inject(ThemeKey) // 类型自动推断为 ThemeContext | undefined
const themeCtxRequired = inject(ThemeKey, { // 提供默认值
theme: ref('light'),
toggle: () => {}
})defineModel(Vue 3.4+)
vue
<!-- 子组件:简化 v-model 实现 -->
<script setup>
// 替代 props.modelValue + emit('update:modelValue')
const model = defineModel<string>()
// 多个 v-model
const title = defineModel<string>('title')
const content = defineModel<string>('content')
</script>
<template>
<input v-model="model" />
</template>
<!-- 父组件使用 -->
<MyInput v-model="text" />
<MyEditor v-model:title="title" v-model:content="content" />异步组件与 Suspense
vue
<script setup>
import { defineAsyncComponent } from 'vue'
// 异步加载组件(代码分割)
const HeavyChart = defineAsyncComponent({
loader: () => import('./HeavyChart.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorDisplay,
delay: 200, // 延迟显示 loading(避免闪烁)
timeout: 3000 // 超时时间
})
</script>
<template>
<!-- Suspense:处理异步组件和 async setup -->
<Suspense>
<template #default>
<HeavyChart />
</template>
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
</template>总结
<script setup>是 Composition API 的语法糖,更简洁- 组合式函数(Composables)是 Vue 3 的逻辑复用方式,替代 Mixins
provide/inject配合InjectionKey实现类型安全的跨层传值defineModel大幅简化 v-model 的实现- 按功能组织代码,而非按选项类型