引言
Vue 3 普及这么久了,我发现一个奇怪的现象:大家的组件并没有变得更整洁,反而变得更乱了。
以前在 Options API 时代,虽然逻辑是跳跃的(data 在上面,methods 在下面),但至少还有个格子供你填空。现在到了 Composition API 时代,script setup 给了一块自由的画布,结果 90% 的开发者把这里当成了新的“垃圾回收站”。
打开你的项目看一眼,是不是所有的变量都堆在文件顶部,所有的函数都堆在底部?如果一个组件有 500 行,你要修一个 bug,是不是还得在第 5 行定义的变量和第 300 行使用的函数之间来回滚动?
这不叫 Composition API,这叫 "Options API in setup"。
核心问题拆解:为什么你写出了“面条代码”?
很多从 Vue 2 转过来的同学都有一个误区:认为 Composition API 的目的是为了“不用 this”或者“更好地支持 Typescript”。
这只是表象。Composition API 的真正核心在于 关注点分离 (Separation of Concerns)。
在 Options API 中,代码是按技术类型组织的(所有的 data 放一起,所有的 computed 放一起)。 在 Composition API 中,代码应该是按逻辑功能组织的(搜索功能相关的 data、methods、computed 放在一起)。
如果你只是把 data 变成了 ref,把 methods 变成了 function,然后依然把它们散落在这一整块 setup 里,那你实际上是在写 "面条代码" (Spaghetti Code) —— 逻辑纠缠不清,难以维护。
Bad Case 展示:
// 你现在的代码是不是长这样?
<script setup>
import { ref, onMounted, watch } from 'vue'
// --- 搜索功能的变量 ---
const keyword = ref('')
const searchResult = ref([])
// --- 分页功能的变量 ---
// (你看,这里已经开始乱了)
const currentPage = ref(1)
const pageSize = ref(10)
// --- 弹窗功能的变量 ---
const showModal = ref(false)
// --- 搜索功能的方法 ---
const handleSearch = () => { ... }
// --- 分页功能的方法 ---
const changePage = () => { ... }
// --- 巨大的 Watch ---
watch(keyword, () => { ... })
</script>
当这个文件膨胀到 500 行时,维护“搜索功能”这一件事,你就需要在第 5 行、第 20 行、第 200 行之间反复横跳。这甚至比 Options API 体验更差。
深度对比与重构 (Refactoring)
如何拯救这种局面?答案是:利用 Composition API 的能力,将逻辑聚合。
Level 1: 简单的逻辑聚合 (Function Scope)
只要把相关联的代码放在一起,就已经迈出了第一步。利用折叠功能,甚至能让代码清晰度提升一个档次。
<script setup>
// Feature A: 搜索逻辑
const { keyword, searchResult, handleSearch } = (() => {
const keyword = ref('')
const searchResult = ref([])
const handleSearch = () => { /* ... */ }
return { keyword, searchResult, handleSearch }
})()
// Feature B: 分页逻辑
const { currentPage, changePage } = (() => {
// ...
})()
</script>
注:实际写代码时不需要用立即执行函数包裹,这只是为了演示“物理位置上的聚合”。你可以直接用注释分隔,并保证相关代码紧凑排列。
Level 2: 提取 Composable (The Real Power)
真正的杀手锏是提取自定义 Hook(Composable)。
// hooks/useSearch.js
export function useSearch(apiParams) {
const keyword = ref("");
const data = ref([]);
const loading = ref(false);
const search = async () => {
loading.value = true;
data.value = await fetch(apiParams.url, { q: keyword.value });
loading.value = false;
};
// 这里的妙处在于:watch 也被封装进去了!
// 组件不再需要关心 keyword 变了要触发什么,Hook 内部自闭环。
watch(keyword, search);
return { keyword, data, loading };
}
// Component.vue
<script setup>
import { useSearch } from './hooks/useSearch'
import { usePagination } from './hooks/usePagination'
// 无论内部逻辑多复杂,在组件层面上只有这两行
const { keyword, data: list, loading } = useSearch({ url: '/api/users' })
const { currentPage, changePage } = usePagination()
</script>
现在,你的组件甚至可能只有 20 行代码。所有的业务逻辑都被拆分到了独立的、可测试的、可复用的文件中。
实战避坑指南 (Best Practices)
在重构过程中,有 3 个最常见的坑,请务必注意:
🚫 坑 1:解构丢失响应性
这是新手最容易犯的错。
// ❌ 错误做法
function useCounter() {
const count = ref(0);
return { count };
}
const { count } = useCounter(); // count 依然是 ref,没问题
// 但是如果是 reactive 对象...
function useUser() {
const state = reactive({ name: "Jack", age: 18 });
return state;
}
const { name } = useUser(); // 💥 完了,name 变成了普通字符串,失去响应性!
✅ 正确做法:尽量多用 ref,如果必须用 reactive 并需要解构返回,请使用 toRefs。
🚫 坑 2:到处都是 Watch
不要把 setup 当作 watch 的大本营。很多人喜欢 watch 一个值然后修改另一个值。
❌ Bad:
const firstName = ref("John");
const lastName = ref("Doe");
const fullName = ref("");
watch([firstName, lastName], () => {
fullName.value = firstName.value + " " + lastName.value;
});
✅ Good:
const fullName = computed(() => firstName.value + " " + lastName.value);
能用 computed 绝不用 watch。computed 是声明式的,而 watch 是命令式的。声明式永远比命令式更好维护。
🚫 坑 3:Props 的陷阱
在 Composable 中接收 props 需要小心。
function useTitle(props) {
// ❌ 错误:如果父组件的 props.title 变了,这里拿到的 title 不会变!
// 因为这是 JS 值传递。
const title = props.title;
// ✅ 正确:传入一个 getter 或者使用 toRef
const titleRef = toRef(props, "title");
}
建议 Composable 的参数设计为:可以接收 ref 或 getter 函数,利用 toValue (Vue 3.3+) 来归一化。
总结与建议
Composition API 是一把双刃剑。它给了你极大的自由,也给了你写出“面条代码”的自由。
自检法则:
如果你的 script setup 超过了 200 行,且没有引入任何本地或外部的 Hook(Composable),那你大概率就在写面条代码。
建议: 下次写代码时,试着先写 Hook,再写组件。你会发现世界大不一样。
互动引导
你的项目里最长的一个 setup 有多少行?1000 行?2000 行?欢迎在评论区“比惨”。