←返回墨迹
/陈墨
Vue 组合式函数设计的三条心法
写了一年组合式函数后沉淀的方法论:单一职责的「作用域」、显式的依赖声明,以及为什么返回值应该是普通对象而不是响应式对象。
从一个坏例子开始
// ❌ 一个什么都想管的 composable
export function useUser() {
const user = ref(null);
const orders = ref([]);
const theme = ref('dark');
async function fetchUser(id: string) { /* ... */ }
async function fetchOrders(id: string) { /* ... */ }
function toggleTheme() { /* ... */ }
return { user, orders, theme, fetchUser, fetchOrders, toggleTheme };
}
这不是组合式函数,这是把组件换个地方写。三个月后没人说得清 theme 为什么住在这里,而 fetchOrders 被三个页面共享,其中两个根本不需要它——但它们都得为这份多余的响应式开销买单。
心法一:按「作用域」切,而不是按「领域」切
组合式函数的粒度单位是一段可复用的响应式作用域,而不是业务实体。领域划分是服务层的职责。所以 useUser 应该裂解成:
useFetchUser(id) // 一次获取 + loading + error
useOrders(userId) // 订单列表的查询与分页
useTheme() // 主题读写与持久化
判断标准很简单:如果两个状态的生命周期或依赖关系不同步,它们就不该住在一起。
心法二:依赖必须从参数进来
// ✅ 依赖显式声明
export function useOrders(userId: Ref<string>) {
const orders = ref([]);
watchEffect(async () => {
orders.value = await api.getOrders(userId.value);
});
return { orders };
}
凡是 composable 内部自己 import 服务、自己读全局单例的,测试时都会让你痛苦。依赖从参数进来,配合 vi.fn() 就能覆盖所有分支;从全局来,你就得连环境一起 mock。
一个例外:真正的应用级单例(如主题)可以用模块级状态,但要显式命名
useThemeStore,让读代码的人知道它不是纯函数。
心法三:返回普通对象,包住响应式引用
// ✅ 调用方拿到的是稳定的 API
export function useCounter() {
const count = ref(0);
function inc() { count.value++; }
return { count, inc };
}
const { count, inc } = useCounter();
// count 是 Ref —— 保持引用,而不是快照
反过来,如果你返回 reactive({ count, inc }),解构会立刻失去响应性,调用方被迫整块持有。Ref 是 Vue 里响应式的「最小包装」,让它保持裸露,生态(watch、computed、模板)才接得上。
结语
三条心法其实是同一句话:让作用域、依赖、返回值都显式。 组合式 API 的自由是「显式的自由」——没有了选项 API 的结构约束,你就得自己把结构立起来。立得住,两百行的逻辑树也能清爽如新;立不住,那就是另一坨 setup 而已。