前言
Vue 3 最引人瞩目的变化之一就是 Composition API 的引入。这一特性从诞生之初就引发了广泛的讨论:有人欢呼它是逻辑复用的终极解决方案,也有人担忧它增加了学习成本。经过几年的实践检验,Composition API 已经证明了自己的价值,它不仅改变了我们编写 Vue 组件的方式,更重要的是彻底解决了困扰 Vue 开发者多年的逻辑复用难题。
本文将深入剖析从 Options API 到 Composition API 的演进之路,通过对比分析,揭示为什么 Composition API 是逻辑复用的未来。
Options API 的局限性
碎片化的代码组织
在 Options API 中,组件的逻辑被强制分割在不同的选项中:data、computed、methods、watch、生命周期钩子。这种组织方式在组件简单时尚可接受,但随着组件复杂度的提升,问题就暴露出来了:
javascript
<script>
export default {
data() {
return {
// 用户相关
user: null,
permissions: [],
// 订单相关
orders: [],
orderLoading: false,
// 商品相关
products: [],
searchKeyword: '',
currentPage: 1,
// UI 状态
modalVisible: false,
sidebarCollapsed: false
}
},
computed: {
// 用户相关
isAdmin() {
return this.permissions.includes('admin')
},
// 订单相关
paidOrders() {
return this.orders.filter(o => o.paid)
},
// 商品相关
filteredProducts() {
return this.products.filter(p =>
p.name.includes(this.searchKeyword)
)
}
},
watch: {
// 用户相关
user: {
handler(newUser) {
this.fetchPermissions(newUser.id)
},
immediate: true
},
// 商品相关
searchKeyword: {
handler() {
this.debouncedSearch()
}
}
},
methods: {
// 用户相关
fetchPermissions() { /* ... */ },
// 订单相关
fetchOrders() { /* ... */ },
markAsPaid() { /* ... */ },
// 商品相关
fetchProducts() { /* ... */ },
debouncedSearch() { /* ... */ },
nextPage() { /* ... */ },
// UI 相关
toggleModal() { /* ... */ },
toggleSidebar() { /* ... */ }
},
created() {
this.fetchPermissions()
this.fetchOrders()
this.fetchProducts()
}
}
</script>
在这个例子中,一个功能(如商品管理)的代码被分散在 data、computed、watch、methods、created 等多个选项中。当我们需要维护某个功能时,不得不在文件中反复上下跳转,这种碎片化的组织方式严重影响了代码的可读性和可维护性。
跨组件逻辑复用困难
当多个组件需要共享相同逻辑时,Vue 2 提供了好几种方案,但每种方案都有明显的缺陷。
Mixins:最常用的复用方式,但问题重重
假如我们在多个 JS 文件中定义了相同的属性: userMixin.js:
javascript
export default {
data() {
return {
user: null, // 这里定义了 user
loading: false
}
},
methods: {
fetchUser() {
this.loading = true
api.getUser().then(user => {
this.user = user
this.loading = false
})
}
},
created() {
this.fetchUser()
}
}
orderMixin.js:
javascript
export default {
data() {
return {
user: null, // 这里也定义了 user,命名冲突!
orders: []
}
},
methods: {
fetchOrders() {
api.getOrders(this.user.id).then(orders => {
this.orders = orders
})
}
}
}
然后我们在组件中使用就出现问题了:
javascript
export default {
mixins: [userMixin, orderMixin],
created() {
console.log(this.user) // 结果是 undefined!因为 orderMixin 覆盖了
}
}
作用域插槽:逻辑复用但模板臃肿
html
<template>
<MouseTracker v-slot="{ x, y }">
<div>鼠标位置: {{ x }}, {{ y }}</div>
</MouseTracker>
</template>
作用域插槽虽然解决了命名冲突问题,但它将逻辑复用局限在模板层面,并且会导致嵌套过深,也就是所谓的槽位嵌套地狱。
TypeScript 支持不友好
Options API 中无处不在的 this 对 TypeScript 来说是个挑战,因为 this 的类型推导是非常困难的:
javascript
export default {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++ // this 的类型推导很困难
}
},
computed: {
doubleCount() {
return this.count * 2 // this.count 的类型?
}
}
}
为了让 Options API 获得良好的类型支持,Vue2 不得不引入 vue-class-component 和 vue-property-decorator,但这些方案又带来了新的复杂性问题。
Mixins 的三大痛点
命名冲突
Mixins 最致命的问题就是命名冲突。当多个 mixin 定义了同名的数据、方法或生命周期钩子时,后一个会覆盖前一个,就如上一节中的例子一样,而且这种覆盖是静默的。这种冲突在大型项目中几乎无法避免,而且由于 JavaScript 的动态特性,这种错误很难在开发阶段被发现。
来源不明:幽灵代码
当我们在模板中看到一个变量 loading,你能快速识别出它来自哪里吗?
html
<template>
<div>
<p v-if="loading">加载中...</p>
<p v-else>{{ userData }}</p>
<button @click="refresh">刷新</button>
</div>
</template>
<script>
import userMixin from './mixins/user'
import loadingMixin from './mixins/loading'
import refreshMixin from './mixins/refresh'
export default {
mixins: [userMixin, loadingMixin, refreshMixin],
// loading 变量是哪个 mixin 提供的?
// refresh 方法是哪个 mixin 提供的?
// userData 又是哪里来的?
}
</script>
这种来源不明 的变量,使得代码的调试变得异常困难。当我们需要修改 loading 的行为时,我们就得在多个 mixin 中查找了,甚至还可能找到一个已经被废弃的代码。
隐式依赖:看不见的耦合
Mixins 之间可能存在隐式的依赖关系,这种依赖没有在代码中显式声明,完全依赖于开发者的理解和文档:
javascript
// baseMixin.js
export default {
data() {
return {
baseData: null
}
}
}
// featureMixin.js
export default {
computed: {
processedData() {
// 隐式依赖:期望 baseMixin 提供 baseData
return this.baseData ? this.baseData.map(item => item.value) : []
}
}
}
// 组件
export default {
mixins: [featureMixin], // ❌ 错误:featureMixin 依赖 baseMixin
// 应该这样:mixins: [baseMixin, featureMixin]
created() {
console.log(this.processedData) // 报错!this.baseData 是 undefined
}
}
这种隐式依赖使得 mixin 的复用变得脆弱不堪。修改一个 mixin 可能会影响到其他的 mixin。
Composition API 的解决方案
按功能组织代码
Composition API 的核心思想是将关注点分离 转变为功能内聚。它允许我们根据功能来组织代码,而不是根据选项类型:
javascript
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
// 用户功能:所有用户相关的代码都在一起
const user = ref(null)
const userLoading = ref(false)
const isAdmin = computed(() => user.value?.permissions?.includes('admin'))
async function fetchUser() {
userLoading.value = true
try {
user.value = await api.getUser()
} finally {
userLoading.value = false
}
}
onMounted(fetchUser)
// 订单功能:所有订单相关的代码都在一起
const orders = ref([])
const ordersLoading = ref(false)
const paidOrders = computed(() => orders.value.filter(o => o.paid))
async function fetchOrders() {
ordersLoading.value = true
try {
orders.value = await api.getOrders()
} finally {
ordersLoading.value = false
}
}
watch(user, fetchOrders, { immediate: true })
// 搜索功能:所有搜索相关的代码都在一起
const keyword = ref('')
const debounceTimer = ref(null)
function search() {
clearTimeout(debounceTimer.value)
debounceTimer.value = setTimeout(() => {
console.log('搜索:', keyword.value)
// 执行搜索逻辑
}, 300)
}
</script>
上述代码中每个功能块的代码都集中在一起,当我们需要维护相关功能时,只需要关注相关的几行代码,而不需要在文件的各个部分跳转。
组合式函数的出现
Composition API 真正的威力在于逻辑的封装和复用,我们可以将上面的功能块提取为独立的组合式函数:
javascript
// composables/useUser.ts
import { ref, computed, onMounted } from 'vue'
export function useUser() {
const user = ref(null)
const loading = ref(false)
const isAdmin = computed(() => user.value?.permissions?.includes('admin'))
async function fetchUser() {
loading.value = true
try {
user.value = await api.getUser()
} finally {
loading.value = false
}
}
onMounted(fetchUser)
return {
user,
loading: loading,
isAdmin,
fetchUser
}
}
// composables/useOrders.ts
import { ref, computed, watch } from 'vue'
export function useOrders(userRef) {
const orders = ref([])
const loading = ref(false)
const paidOrders = computed(() => orders.value.filter(o => o.paid))
async function fetchOrders() {
if (!userRef.value?.id) return
loading.value = true
try {
orders.value = await api.getOrders(userRef.value.id)
} finally {
loading.value = false
}
}
watch(userRef, fetchOrders, { immediate: true })
return {
orders,
loading,
paidOrders,
fetchOrders
}
}
// 在组件中使用
<script setup>
import { useUser } from './composables/useUser'
import { useOrders } from './composables/useOrders'
const { user, loading: userLoading, isAdmin } = useUser()
const { orders, loading: ordersLoading, paidOrders } = useOrders(user)
</script>
显式的依赖关系
相比 mixins 的隐式依赖,Composition API 的依赖关系是显式的、类型安全的:
javascript
const { user } = useUser() // 明确声明依赖 user
const { orders } = useOrders(user)
// 多个来源也很清晰,依赖都可见
const { data: productData } = useProducts()
const { data: categoryData } = useCategories()
const { combinedData } = useCombinedData(productData, categoryData)
这种显式依赖有巨大的优势:
- 代码可读性:一眼就能看出函数需要什么参数
- 类型安全:TypeScript 会在编译阶段检查参数类型
- 易于测试:可以轻松传入
mock数据 - 避免冲突:通过变量重命名解决命名冲突
完美的 TypeScript 支持
由于组合式函数就是普通的 JavaScript 函数,TypeScript 的类型推导可以完美工作:
javascript
// useCounter.ts
import { ref } from 'vue'
export function useCounter(initialValue: number = 0) {
const count = ref(initialValue)
function increment(step: number = 1) {
count.value += step
}
function decrement(step: number = 1) {
count.value -= step
}
return {
count, // Ref<number>
increment, // (step?: number) => void
decrement // (step?: number) => void
}
}
// 组件中使用
const { count, increment } = useCounter(10)
count.value // 类型为 number
increment(5) // 参数类型检查
何时使用 Composition API,何时保留 Options API?
尽管 Composition API 优势明显,但它并不是要完全替代 Options API,两者各有适用的场景。
适合 Composition API 的场景
- 复杂业务组件:当组件的逻辑复杂度较高,包含多个功能模块时,Composition API 的组织优势就体现出来了
- 可复用逻辑:需要在多个组件间共享的逻辑,封装为组合式函数是最佳选择
- 大型项目:随着项目规模的扩大,Composition API 的优势会越来越明显,代码的组织和复用都会更加容易
适合 Options API 的场景
- 简单展示组件:对于只包含少量 props 和模板的简单组件,Options API 的简洁性反而是优势。
- 学习入门:对于刚开始学习 Vue 的开发者,Options API 的概念更直观,更容易理解。
- 现有项目迁移:对于大型的 Vue2 项目,完全重构到 Composition API 成本较高,可以混合使用,逐步迁移。
最佳实践:何时选择哪种模式
| 场景 | 推荐模式 | 原因 |
|---|---|---|
| 简单展示组件 | Options API | 简洁直观 |
| 复杂业务组件 | Composition API | 逻辑组织清晰 |
| 可复用逻辑 | Composition API | 封装为组合式函数 |
| 小型项目 | 两者皆可 | 根据团队偏好 |
| 大型项目 | Composition API | 长期维护性更好 |
| 新手学习 | Options API | 概念更容易理解 |
| 专业开发 | Composition API | TS类型支持更好 |
结语
从 Options API 到 Composition API 的演进,反映了前端开发对逻辑复用和代码组织不断深入的理解。Composition API 的出现不是要推翻 Vue 的核心设计,而是为开发者提供更强大的工具来解决复杂场景下的问题。
对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!