写了一段时间 Composition API 之后,我发现真正决定可维护性的并不是“用没用 ref / reactive”,而是逻辑怎么组织。把这阵子踩过的坑整理成几条简单的实践,自己复盘也方便其他人参考。
1. 用 composable 拆“一团乱麻”的 setup
setup 里如果出现“数据 + 副作用 + 事件 + 业务规则”全部堆在一起,就该往外抽了。一个判断标准:它能不能被另一个组件复用? 如果能,就给它一个 useXxx 的名字。
// composables/useToggle.ts
export function useToggle(initial = false) {
const value = ref(initial)
const toggle = () => (value.value = !value.value)
return { value, toggle }
}哪怕只被一处用到,抽出来也能让 setup 变成“一句话讲清楚组件在干嘛”。
2. ref 还是 reactive?优先用 ref
两个都能用,但 ref 有几个无可替代的好处:
- 可以直接被解构和返回(
reactive解构会丢响应式) - 在
<template>里自动解包,书写体验和data()一样 - 包基本类型时语义更清晰
只有一种场景我会优先选 reactive:确定是一个对象,且不需要整体替换,例如一个表单 model。
3. 异步状态用 useAsyncData 或自封装的“三件套”
不要在 setup 里随手 await fetch(...),会让初始渲染卡住、错误处理也变得分散。统一的封装至少返回三样东西:
const { data, pending, error, refresh } = useAsyncData('posts', () => $fetch('/api/posts'))自己封装时也尽量贴近这个形状,所有调用点的心智模型就是一致的。
4. watch 的依赖一定要显式
// 不好:依赖什么不清楚,容易触发多次
watchEffect(() => { fetchSomething(route.params.id, filter.value) })
// 更清晰:依赖列出来,行为可控
watch(
[() => route.params.id, filter],
([id, f]) => fetchSomething(id, f),
{ immediate: true },
)watchEffect 适合“纯响应式副作用”,业务请求建议老老实实写 watch,既能精确控制 immediate / flush,也方便后续加上 { deep: true } / { once: true } 等选项。
5. 组件之间“传东西”,先想清楚方向
| 方向 | 推荐方式 |
|---|---|
| 父 → 子 | defineProps |
| 子 → 父 | defineEmits |
| 跨多层 | provide / inject(或一个轻量 store) |
| 全局 | Pinia / useState / 自定义 composable |
一个简单原则:能用 props/emits 解决就不要 provide,能用 provide 解决就不要全局 store。每往“更全局”的方向走一步,可测性和可读性都会下降一点。
小结
Composition API 给了我们把逻辑组织好的工具,但工具本身不会替我们做决策。把 setup 当成“目录”,把细节藏进 useXxx 里,代码就会比之前清晰一大截。