扇贝听力
126.68M · 2026-02-26
假设我们有一个页面:
在 Vue2 里我们会这样写:
迁移到 Vue.js 3 后,很多人会写成这样:
setup() {
const route = useRoute()
const detail = ref(null)
const queryDetailLoading = ref(false)
const user = ref(null)
const confirmLoading = ref(false)
async function queryDetail(id: string) {
queryDetailLoading.value = true
detail.value = await api.getDetail(id)
queryDetailLoading.value = false
}
async function queryUser(id: string) {
user.value = await api.getUser(id)
}
async function confirm(id: string) {
confirmLoading.value = true
await api.confirm(id)
confirmLoading.value = false
}
onMounted(() => {
queryDetail(route.params.id)
queryUser(route.params.id)
})
return {
detail,
queryDetailLoading,
user,
confirmLoading,
confirm
}
}
看起来没问题,甚至代码还很整洁:
但是,代码的组织度好像没有变化?上面的代码好像还是很容易无序膨胀,出现几千行的 .vue 文件?应该怎么抽象?
Vue 官网有张对比图:
很多人以为它表达的是:
但它真正表达的是:
Options API 是:
按类型组织。
而 Composition API,可以按照:
功能模块组织代码。
先把同一职责的代码写在一起。
setup() {
const route = useRoute()
// ===== 详情模块 =====
const detail = ref(null)
const queryDetailLoading = ref(false)
async function queryDetail(id: string) {
queryDetailLoading.value = true
detail.value = await api.getDetail(id)
queryDetailLoading.value = false
}
onMounted(() => {
queryDetail(route.params.id)
})
// ===== 用户模块 =====
const user = ref(null)
async function queryUser(id: string) {
user.value = await api.getUser(id)
}
onMounted(() => {
queryUser(route.params.id)
})
// ===== confirm 模块 =====
const confirmLoading = ref(false)
async function confirm(id: string) {
confirmLoading.value = true
await api.confirm(id)
confirmLoading.value = false
}
return {
detail,
queryDetailLoading,
user,
confirmLoading,
confirm
}
}
这里有个 Vue 2 很容易忽略的思维定式:
这一刻,setup 不再是一个“大仓库”,而是一个“功能组合器”。
生命周期不再是“一个入口”,它可以属于不同功能块。
这样的代码组织,才是官网对比图中的样子。
在 setup 中拆分功能块后,可以很自然地将各个功能块拆分出 setup。
比如对于详情模块,输入是 route.params.id,输出是 detail 和 queryDetailLoading
// ===== 详情模块 =====
const detail = ref(null)
const queryDetailLoading = ref(false)
async function queryDetail(id: string) {
queryDetailLoading.value = true
detail.value = await api.getDetail(id)
queryDetailLoading.value = false
}
onMounted(() => {
queryDetail(route.params.id)
})
重构为
function useDetail(id: string) {
const detail = ref(null)
const queryDetailLoading = ref(false)
async function queryDetail(id: string) {
queryDetailLoading.value = true
detail.value = await api.getDetail(id)
queryDetailLoading.value = false
}
onMounted(() => queryDetail(id))
return {
detail,
queryDetailLoading
}
}
于是 setup 成为
setup() {
const route = useRoute()
// ===== 详情模块 =====
const { detail, queryDetailLoading } = useDetail(route.params.id)
// ===== 用户模块 =====
const { user } = useUser(route.params.id)
// ===== confirm 模块 =====
const { confirm, confirmLoading } = useConfirm()
return {
detail,
queryDetailLoading,
user,
confirmLoading,
confirm
}
}
通过这种方式,各个 composition 有自己的职责,setup 负责视图层数据的聚合,你再不会写出流水账式的代码。
到这里,其实我们已经完成了“功能拆分”。
但还有一个更重要的转变:setup 不应该围绕生命周期组织,而应该围绕数据变化组织。
比如经常遇到的问题:如果路由参数变化要怎么做呢?
实际这里的逻辑是,当 id 变化时,重新获取 detail
function useDetail(id: MaybeRefOrGetter<string>) {
// ...
watch(() => toValue(id), () => queryDetail(toValue(id)), { immediate: true })
return {
detail,
queryDetailLoading
}
}
setup() {
const route = useRoute()
// ===== 详情模块 =====
const { detail, queryDetailLoading } = useDetail(() => route.params.id)
// ...
return {
detail,
queryDetailLoading,
// ...
}
}
如果使用了 将 props 传递给路由组件,还可以将 route 统一到组件标准的 props 操作:
setup(props) {
// ===== 详情模块 =====
const { detail, queryDetailLoading } = useDetail(() => props.id)
// ...
return {
detail,
queryDetailLoading,
// ...
}
}
升级路径其实很自然:
Vue3 没让代码变乱。它提供了按功能组织代码的能力。
它的真正价值,不在于 setup,而在于它允许我们按“业务模型”组织代码,而不是按“框架结构”组织代码。
这可以让我们写出更内聚,更单一职责的代码。
如果你已经接受“按功能组织 + 数据驱动副作用”这个思路,那么其实可以再进一步,把这些模式固化下来。
在很多场景下,setup 中的内容没有复用的必要,单独抽到其它文件中有点大材小用。
有没有不拆分,还能保证各功能块高度内聚的写法?我写了 vue-asyncx 用于解决这个问题。
setup(props) {
// ===== 详情模块 =====
const detail = ref(null)
const queryDetailLoading = ref(false)
async function queryDetail(id: string) {
queryDetailLoading.value = true
detail.value = await api.getDetail(id)
queryDetailLoading.value = false
}
watch(() => props.id, () => queryDetail(props.id), { immediate: true })
// ===== 用户模块 =====
const user = ref(null)
async function queryUser(id: string) {
user.value = await api.getUser(id)
}
watch(() => props.id, () => queryUser(props.id), { immediate: true })
// ===== confirm 模块 =====
const confirmLoading = ref(false)
async function confirm(id: string) {
confirmLoading.value = true
await api.confirm(id)
confirmLoading.value = false
}
return {
detail,
queryDetailLoading,
user,
confirmLoading,
confirm
}
}
可以重构为
import { useAsyncData, useAsync } from 'vue-asyncx'
setup(props) {
// ===== 详情模块 =====
const {
detail,
queryDetailLoading
} = useAsyncData('detail', () => api.getDetail(props.id), {
watch: () => props.id, immediate: true
})
// ===== 用户模块 =====
const { user } = useAsyncData('user', () => api.getUser(props.id), {
watch: () => props.id, immediate: true
})
// ===== confirm 模块 =====
const { confirm, confirmLoading } = useAsync('confirm', (id: string) => api.confirm(id))
return {
detail,
queryDetailLoading,
user,
confirmLoading,
confirm
}
}
核心代码从19行减少到10行,代码量减少接近 50%,而语义化、组织性不丢失。
更多详细用法,见:早点下班:在 Vue3 中少写 40%+ 的异步代码