10.1 Composition API:按功能组织逻辑 本节摘要:选项式 API 把一个功能的代码拆散到 data、methods、computed、watch 四处,功能一多就互相缠绕。Composition API 允许把一个关注点的数据、计算、侦听、方法写在一起,并抽成可复用的组合函数。本节覆盖 setup 语法、ref 与 reactive 的分工、生命周期对位,以及组合函数的抽取范式。 学习目标 阅读完本节,你应当能够: 用 script setup 写出等价于选项式的完整组件; 说清 ref 与 reactive 的取舍规则与解构陷阱的成因; 把一段业务逻辑抽成规范的组合函数; 判断什么逻辑值得抽、什么留在组件里。
本节摘要:选项式 API 把一个功能的代码拆散到 data、methods、computed、watch 四处,功能一多就互相缠绕。Composition API 允许把一个关注点的数据、计算、侦听、方法写在一起,并抽成可复用的组合函数。本节覆盖 setup 语法、ref 与 reactive 的分工、生命周期对位,以及组合函数的抽取范式。
阅读完本节,你应当能够:
一个"搜索"功能,选项式下的代码散落四处:
export default { data() { return { keyword: '', results: [], loading: false // 搜索相关状态在这里 }; }, computed: { hasResult() { return this.results.length > 0; } // 搜索相关计算在这里 }, watch: { keyword(v) { this.do_search(v); } // 搜索相关侦听在这里 }, methods: { async do_search(v) { /* 搜索相关方法在这里 */ } } // 再加一个分页功能、一个筛选功能……四格里各自膨胀,读代码来回跳 };
组件超过两百行、功能超过三个,"找一个功能的完整实现"就开始翻页跳跃。选项式的分格按代码类型组织,而人的理解按功能组织——错位是结构性的,靠自觉克服不了。
<script setup> import { ref, computed, watch } from 'vue'; // ---- 搜索功能:状态 计算方法侦听 一屏之内 ---- const keyword = ref(''); const results = ref([]); const loading = ref(false); const hasResult = computed(() => results.value.length > 0); watch(keyword, (v) => do_search(v)); async function do_search(v) { loading.value = true; results.value = await api.search(v); loading.value = false; } // ---- 分页功能:紧跟其后 互不缠绕 ---- const page = ref(1); function next() { page.value++; } </script> <template> <input v-model="keyword"> <ul v-if="hasResult"><li v-for="r in results" :key="r.id">{{ r.title }}</li></ul> </template>
script setup 是编译期语法糖:顶层变量自动暴露给模板,组件导入即注册。this 消失了——这是有意为之,this 在选项式里是运行时绑定的黑盒,去掉后代码全是普通变量与函数,类型推导、按需引用、单元测试全部顺畅。
const count = ref(0); // 基本值:必须用 ref 包进对象 const user = reactive({ name: '张三', age: 20 }); // 对象:可直接代理
ref 通过 value 属性读写(脚本里 count.value,模板里自动解包省掉 value),reactive 直接点取字段。分工建议明确化:基本值与需要整体替换的引用用 ref,结构数据组用 reactive;混着用容易出事,全 ref 也是一个自洽的流派——团队定一条即可。
reactive 有一个必须理解的陷阱:解构即失联。
const { name } = user; // name 是普通字符串,与响应式断开 name = '李四'; // 视图不会更新 // 要保持响应式的解构,用工具函数 const { name: r_name } = toRefs(user); // 每个字段变成 ref,仍指向源
成因第 2 章讲过:Proxy 代理的是对象,解构取出的是值本身,离开代理对象就没人拦截读写了。参数传递同理——把 reactive 对象传给函数,函数内整体替换参数变量不会影响源。
| Vue 2 | 组合式(setup 内) |
|---|---|
| beforeCreate / created | setup 本身(执行时机即取代这两者) |
| beforeMount | onBeforeMount |
| mounted | onMounted |
| beforeUpdate | onBeforeUpdate |
| updated | onUpdated |
| beforeDestroy / destroyed | onBeforeUnmount / onUnmounted |
created 没有对位函数,因为 setup 本身就在那个时机执行——组合式里"初始化逻辑"就是 setup 顶层代码。
组件复用结构(插槽),组合函数复用逻辑。把上面的搜索抽出来:
// useSearch.js —— 一个规范的小组合函数 import { ref, computed } from 'vue'; export function useSearch(fetchFn, delay = 300) { const keyword = ref(''); const results = ref([]); const loading = ref(false); const hasResult = computed(() => results.value.length > 0); let timer = null; function on_input(v) { clearTimeout(timer); timer = setTimeout(() => do_search(v), delay); // 防抖内建 } async function do_search(v) { if (!v) { results.value = []; return; } loading.value = true; try { results.value = await fetchFn(v); } finally { loading.value = false; } } return { keyword, results, loading, hasResult, on_input }; }
任何组件两行接入:
const { keyword, results, loading, on_input } = useSearch(api.search);
规范要点:命名以 use 开头;入参收函数而非写死接口(可测试性);返回 ref 集合;副作用(定时器、监听)在函数内用生命周期钩子成对清理。它对比 Vue 2 时代混入(mixin)的优势是显式——来源看得见(import 语句)、命名冲突可解(解构重命名),混入那套"魔法注入、来源成谜、命名撞车"三大罪就此终结。

组合式不是政治正确:几十行的小组件,选项式反而一眼可读;团队全是选项式存量、没有 TypeScript 计划,迁移收益有限。我的分界线:组件功能超过三个、或逻辑需要跨组件复用、或项目用 TypeScript——满足任一条就切,否则两种都合法。Vue 3 对两种风格是一等支持,不是替代关系。
组合函数还有一个隐性收益值得点破:它让"逻辑的边界"从组件里被拉出来,变成了可以被审视、被测试、被交接的独立单元。选项式时代,一段业务逻辑的完整行为分散在 data、methods、watch 里,交接时要读整个组件;组合函数时代,接手人看一个文件的输入输出就能复述行为。代码评审也因此有了更细的抓手——评审一个几十行的组合函数,比评审一个三百行的大组件容易做出实质性判断。这其实是本章所有语法变化的最终落点:写法的改变,服务于逻辑组织方式的改变。
写法升级之外,框架本身改了什么?下一节过 Vue 3 的特性与性能账本。