Skip to content

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 的实现
  • 按功能组织代码,而非按选项类型

系统学习 Web 前端生态,深入底层架构