背景

Vue3 发布两年多了,从 Options API 迁移到 Composition API 的项目也有了不少。这里总结一些实战经验。

为什么需要 Composition API

Options API 的问题:逻辑关注点分散在一个组件的各个选项里(datamethodscomputedwatch…)。

// Options API - 逻辑分散
export default {
  data() {
    return { count: 0 }
  },
  methods: {
    increment() { this.count++ }
  },
  computed: {
    doubled() { return this.count * 2 }
  },
  watch: {
    count(newVal) {
      console.log('count changed:', newVal)
    }
  }
}
<!-- Composition API - 逻辑内聚 -->
<script setup>
import { ref, computed, watch } from 'vue'

const count = ref(0)
const doubled = computed(() => count.value * 2)

const increment = () => count.value++

watch(count, (newVal) => {
  console.log('count changed:', newVal)
})
</script>

实用技巧

1. ref vs reactive 该用哪个?

// primitive types (String, Number, Boolean) -> ref
const name = ref('BvBeJ')
const age = ref(18)

// objects/arrays -> reactive
const user = reactive({
  name: 'BvBeJ',
  skills: ['Go', 'Rust', 'C++']
})

// 或者对象也用 ref,通过 .value 访问
const user = ref({ name: 'BvBeJ' })
user.value.name = 'New Name'  // 需要 .value

我的习惯: 简单类型用 ref,复杂对象用 reactive。TypeScript 类型推导更清晰。

2. 自定义 Hooks:逻辑复用

// useWindowSize.ts
import { ref, onMounted, onUnmounted } from 'vue'

export function useWindowSize() {
  const width = ref(window.innerWidth)
  const height = ref(window.innerHeight)

  const update = () => {
    width.value = window.innerWidth
    height.value = window.innerHeight
  }

  onMounted(() => window.addEventListener('resize', update))
  onUnmounted(() => window.removeEventListener('resize', update))

  return { width, height }
}

// 组件中使用
<script setup>
import { useWindowSize } from '@/hooks/useWindowSize'

const { width, height } = useWindowSize()
</script>

3. provide / inject 替代 Vuex/Pinia?

对于简单场景,provide/inject 比 Pinia 更轻量:

<!-- Parent.vue -->
<script setup>
import { provide } from 'vue'

const theme = ref('dark')
provide('theme', theme)
</script>

<!-- Child.vue -->
<script setup>
import { inject } from 'vue'

const theme = inject('theme')
</script>

注意: provide 的是响应式的,但子组件修改会影响父组件,小心副作用。

4. 异步组件与 Suspense

<script setup>
import { defineAsyncComponent } from 'vue'

const HeavyComponent = defineAsyncComponent(() =>
  import('./HeavyComponent.vue')
)
</script>

<template>
  <Suspense>
    <template #default>
      <HeavyComponent />
    </template>
    <template #fallback>
      <LoadingSpinner />
    </template>
  </Suspense>
</template>

TypeScript 集成

<script setup lang="ts">
interface User {
  name: string
  age: number
  skills: string[]
}

const user = ref<User | null>(null)

// 泛型指定类型
const list = ref<User[]>([])

// with default
const count = ref<number>(0)
</script>

常见坑

  1. 解构 reactive 对象会丢失响应式
const user = reactive({ name: 'BvBeJ', age: 18 })
const { name } = user  // ❌ name 失去响应式

// 正确做法:
const name = toRef(user, 'name')  // ✅
  1. watch 监听 reactive 对象属性
const user = reactive({ count: 0 })

// ❌ 错误 - 第一个参数需要 getter
watch(() => user.count, (newVal) => {
  console.log(newVal)
})

总结

Composition API 让我们能像写函数一样组织组件逻辑,更容易抽取、更容易测试、更容易 TypeScript 化。

建议: 新项目直接用 Composition API,老项目逐步迁移核心逻辑。Vue3 的 <script setup> 语法糖是真的香。


你更喜欢 Options API 还是 Composition API?欢迎留言讨论。