为什么组合式API比选项式API更适合复杂业务
Vue3的组合式API(Composition API)不是语法糖,而是解决选项式API(Options API)在复杂业务场景下逻辑碎片化问题的架构方案。选项式API把同一个业务逻辑的data、methods、computed、watch分散到不同选项中,一个”用户搜索+筛选+分页”的功能,代码横跨4个选项块,维护时需要反复跳转。组合式API把相关逻辑聚合到同一个composable函数中,逻辑内聚,复用清晰。
组合式API的核心原则:按功能聚合,非按类型分散。一个useUserSearch函数包含搜索所需的全部状态和方法,外部只需调用返回值即可使用,无需关心内部实现。
Composable函数设计模式
Composable函数的命名约定以use开头,参数接收外部依赖,返回响应式状态和方法。以下是一个完整的数据表格composable:
// useDataTable.ts - 通用数据表格逻辑封装
import { ref, computed, watch } from 'vue'
import type { Ref } from 'vue'
interface DataTableOptions<T> {
fetchData: (params: Record<string, any>) => Promise<{ list: T[]; total: number }>
pageSize?: number
immediate?: boolean
}
export function useDataTable<T>(
options: DataTableOptions<T>
) {
const data = ref<T[]>([]) as Ref<T[]>
const loading = ref(false)
const currentPage = ref(1)
const pageSize = ref(options.pageSize || 20)
const total = ref(0)
const searchParams = ref<Record<string, any>>({})
const totalPages = computed(() => Math.ceil(total.value / pageSize.value))
async function fetchList() {
loading.value = true
try {
const params = {
...searchParams.value,
page: currentPage.value,
pageSize: pageSize.value,
}
const result = await options.fetchData(params)
data.value = result.list
total.value = result.total
} catch (error) {
console.error('fetchData failed:', error)
data.value = []
} finally {
loading.value = false
}
}
function handlePageChange(page: number) {
currentPage.value = page
fetchList()
}
function handleSearch(params: Record<string, any>) {
searchParams.value = params
currentPage.value = 1
fetchList()
}
if (options.immediate !== false) {
fetchList()
}
return {
data,
loading,
currentPage,
pageSize,
total,
totalPages,
fetchList,
handlePageChange,
handleSearch,
}
}
组件中使用:
<script setup lang="ts">
import { useDataTable } from '@/composables/useDataTable'
const {
data: userList,
loading,
currentPage,
total,
handlePageChange,
handleSearch,
} = useDataTable({
fetchData: (params) => api.getUserList(params),
pageSize: 20,
})
</script>
复杂表单场景的组合式API封装
复杂表单是前端最容易出现逻辑膨胀的场景。一个包含多步骤、动态校验、联动的表单,用选项式API写出上千行毫不罕见。用组合式API可以按步骤或功能模块拆分:
// useOrderForm.ts - 订单表单逻辑
import { ref, reactive, computed } from 'vue'
export function useOrderForm() {
const currentStep = ref(0)
const formState = reactive({
customerName: '',
contactPhone: '',
items: [] as Array<{ skuId: string; quantity: number; price: number }>,
shippingMethod: '',
shippingAddress: '',
})
const validators = {
step0: () => !!formState.customerName && /^1\d{10}$/.test(formState.contactPhone),
step1: () => formState.items.length > 0 && formState.items.every(i => i.quantity > 0),
step2: () => !!formState.shippingMethod && !!formState.shippingAddress,
}
const canProceed = computed(() => {
const validator = validators[`step${currentStep.value}` as keyof typeof validators]
return validator ? validator() : false
})
const totalAmount = computed(() =>
formState.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
function nextStep() {
if (canProceed.value && currentStep.value < 2) {
currentStep.value++
}
}
function prevStep() {
if (currentStep.value > 0) currentStep.value--
}
function addItem(skuId: string, price: number) {
const existing = formState.items.find(i => i.skuId === skuId)
if (existing) {
existing.quantity++
} else {
formState.items.push({ skuId, quantity: 1, price })
}
}
return {
currentStep,
formState,
canProceed,
totalAmount,
nextStep,
prevStep,
addItem,
}
}
Composable的依赖注入与测试
Composable函数的依赖应该通过参数注入,而非在函数内部硬编码。这既是设计原则,也是测试的前提。反例:
// 反例:硬编码API调用,无法测试
export function useUserData() {
const fetch = async () => {
const res = await axios.get('/api/users') // 硬编码
return res.data
}
}
正例:依赖通过参数注入,可替换为mock:
// 正例:依赖注入
export function useUserData(api: { getUsers: () => Promise<User[]> }) {
const fetch = async () => {
return await api.getUsers() // 可替换
}
}
// 测试时注入mock
const mockApi = { getUsers: vi.fn().mockResolvedValue([{ id: 1, name: 'test' }]) }
const { fetch } = useUserData(mockApi)
性能优化:shallowRef与computed的正确使用
组合式API场景下,性能问题常出现在响应式追踪范围过大。深层reactive对象中任何一个属性变化都会触发依赖它的effect重新执行。当数据量较大时(如1000行的表格数据),用shallowRef替代ref可以避免深层追踪:
// 大数据量表格用shallowRef避免深层响应式追踪
const tableData = shallowRef<TableRow[]>([])
// 更新数据时,创建新数组触发引用变化
function updateRow(index: number, row: TableRow) {
const newData = [...tableData.value]
newData[index] = { ...newData[index], ...row }
tableData.value = newData // 浅层引用变更,触发更新
}
// 避免的做法:直接修改属性,不会触发更新
// tableData.value[index].name = 'new name' // 无效!
computed的缓存机制也值得注意。computed只在依赖变化时重新计算,但Vue3.4之前computed有内部dirty标记的优化问题。大量computed依赖同一个reactive对象的多个属性时,任一属性变化都会让所有computed重新计算。拆分大reactive为多个小ref可以缓解:
// 拆分前:一个大reactive导致computed过度重算
const state = reactive({ a: 1, b: 2, c: 3 })
const computedA = computed(() => state.a * 2) // b或c变化也会触发
// 拆分后:独立ref,精确追踪
const a = ref(1)
const b = ref(2)
const c = ref(3)
const computedA = computed(() => a.value * 2) // 只有a变化才触发
组合式API不是银弹,但它在复杂业务逻辑的组织上确实优于选项式API。核心实践原则:按功能聚合而非按类型分散、依赖注入而非硬编码、大数据量用shallowRef避免深层追踪。掌握这些模式,才能在真实项目中写出可维护、可测试、高性能的Vue3代码。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-api-feng-zhuang-fu-za-ye-wu-luo-ji-de-shi/