setup 与组合式 API 组织方式
组合式 API 与生命周期 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
这一节讲清楚「组合式 API」到底组合了什么:为什么 Vue 3 要从 Options API 转向它、<script setup> 的执行时机、以及如何按「功能」而不是「选项」组织代码。学完你能把一个按 data/methods/computed 平铺的组件改写成按功能分块的组合式写法。
问题场景
一个搜索页组件有 300 行:data 里躺着关键词、结果、加载态、分页参数;methods 里散落着搜索、翻页、重置;computed 里混着格式化逻辑。改一个「防抖搜索」功能,要在文件里上下跳六次。Options API 的痛点不是难写,是同一功能的代码被拆散在不同选项里,文件一大就失去局部性。
核心解释
组合式 API 的核心主张:按逻辑关注点组织代码,而不是按选项类型。一个功能相关的状态、派生、方法、副作用写在相邻的几行里:
<script setup>
import { computed, ref, watch } from "vue";
// —— 功能一:搜索 ——
const keyword = ref("");
const debounced = ref("");
let timer = null;
watch(keyword, (v) => {
clearTimeout(timer);
timer = setTimeout(() => (debounced.value = v), 300);
});
// —— 功能二:结果列表 ——
const results = ref([]);
const loading = ref(false);
watch(debounced, async (q) => {
if (!q) return;
loading.value = true;
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`);
results.value = await res.json();
} finally {
loading.value = false;
}
});
// —— 功能三:派生统计 ——
const count = computed(() => results.value.length);
const summary = computed(() =>
loading.value ? "搜索中…" : `共 ${count.value} 条结果`
);
</script>
<template>
<input v-model="keyword" placeholder="搜索" />
<p>{{ summary }}</p>
<ul>
<li v-for="r in results" :key="r.id">{{ r.title }}</li>
</ul>
</template>
三段代码各自闭环,删掉「搜索」功能只需删一段。对比 Options API:同样的逻辑要拆到 data 的 6 个字段、watch 的 2 个条目、computed 的 2 个条目、methods 的 3 个方法里。
<script setup> 是语法糖,编译后等价于 setup() 函数返回一个对象。执行时机:在组件实例创建时、beforeCreate 之后、created 之前同步执行一次;此时 DOM 还没生成,所以 document.querySelector 拿不到组件内的元素——需要 DOM 就放 onMounted。
顶层绑定自动暴露给模板:const count = ref(0) 声明后模板直接用 count,不需要 return。import 的组件、自定义指令同理自动注册。这带来一个限制:<script setup> 里不能有默认导出(export default 是编译宏生成的)。
需要和 Options API 混用(老项目渐进迁移)时,可以再加一个普通 <script> 块只做 export default { name: "..." } 这类声明。新代码不需要。
示例拆解
把 React 的 custom hook 思维迁移过来:Vue 的组合式 API 允许你把一段逻辑抽成函数,在 setup 里调用。这是 V13 自定义 composable 的预告,先感受一下:
// composables/useCounter.js
import { computed, ref } from "vue";
export function useCounter(initial = 0, step = 1) {
const count = ref(initial);
const double = computed(() => count.value * 2);
function increment() {
count.value += step;
}
function reset() {
count.value = initial;
}
return { count, double, increment, reset };
}
<script setup>
import { useCounter } from "../composables/useCounter";
const { count, double, increment, reset } = useCounter(10, 2);
</script>
<template>
<p>{{ count }} / 双倍 {{ double }}</p>
<button @click="increment">+2</button>
<button @click="reset">重置</button>
</template>
注意返回值解构后 count 仍是 ref(composable 返回的是 ref 本身,解构不丢响应式——和 reactive 不同)。多个组件调用 useCounter 各自持有独立状态,这就是「逻辑复用」:React 的 custom hook 和 Vue 的 composable 在这个层面几乎同构。
动手任务
- 把本节开头的搜索组件按功能分块重写,每块加一行注释标题,感受局部性。
- 把
useCounter复制到自己的项目里,让两个组件各用一个,确认状态互不影响。 - 在
<script setup>顶层写console.log(document.querySelector("h1")),在onMounted里再写一次,对比输出。
常见错误
- 在 setup 顶层操作 DOM:组件还没挂载,元素不存在。放
onMounted。 - 以为
<script setup>能写export default:编译报错。需要命名组件用defineOptions({ name })。 - setup 里用
this:组合式 API 没有this,访问组件实例用getCurrentInstance()(不推荐日常使用)。 - 把 composable 当普通工具函数在事件里调用:
useCounter()内部用了 ref,必须在 setup 同步调用栈里执行;在onClick里调用会脱离组件上下文,钩子失效。
检查与复盘
- 能说出组合式 API 相对 Options API 的核心优势(逻辑局部性)
- 能准确说出
<script setup>的执行时机,以及它和 onMounted 的先后 - 能写出一个最简单的 composable 并在两个组件里复用
参考资料
- Vue 官方文档「组合式 API 常见问答」:设计动机与对比
- Vue 官方文档「生命周期钩子」:执行时机
读完这一课了?
标记完成后,工作台会安排接下来的学习。