背景
Vue3 发布两年多了,从 Options API 迁移到 Composition API 的项目也有了不少。这里总结一些实战经验。
为什么需要 Composition API
Options API 的问题:逻辑关注点分散在一个组件的各个选项里(data、methods、computed、watch…)。
// 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>
常见坑
- 解构 reactive 对象会丢失响应式
const user = reactive({ name: 'BvBeJ', age: 18 })
const { name } = user // ❌ name 失去响应式
// 正确做法:
const name = toRef(user, 'name') // ✅
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?欢迎留言讨论。