Options API 的问题是「一个功能的代码散落在 data、computed、watch、methods 四个地方」;Composition API 把同一功能的代码收进一个函数。收益是真的,但前提是状态与副作用的边界划清楚,否则只是把分散变成了混乱。

Vue 3 Composition API:ref、reactive 与副作用 示意图

ref 与 reactive 怎么选

结论先给:默认用 ref。它能装任意类型,解构不丢响应性,传递时也不会因为「换了个对象」而断开追踪。

import { ref, reactive } from 'vue';

const keyword = ref('');
const filter = reactive({ status: 'paid', page: 1 });

Object.assign(filter, { status: 'refunded', page: 1 });

reactive 适合字段固定、成组使用的表单对象;一旦需要整体替换(例如从接口拉回一份新配置),就该用 ref。注意 reactive 对象解构出来的字段会失去响应性,必须配合 toRefs。

组合函数:只暴露必要的状态

import { ref, onScopeDispose } from 'vue';

export function usePolling(fetcher, interval = 5000) {
    const data = ref(null);
    const loading = ref(false);
    let timer = null;

    async function tick() {
        loading.value = true;
        try {
            data.value = await fetcher();
        } finally {
            loading.value = false;
        }
    }

    tick();
    timer = setInterval(tick, interval);
    onScopeDispose(() => clearInterval(timer));

    return { data, loading, refresh: tick };
}

三点值得注意:返回值用对象而不是数组,调用方按需解构;定时器句柄用普通 let 保存,它不需要被追踪;清理逻辑挂在 onScopeDispose 上,组件卸载或 effect scope 销毁时都会执行,比 onUnmounted 的适用范围更广。

watch 与 watchEffect 的取舍

  • 需要旧值、需要控制触发时机(flush: 'post')、需要精确指定来源时,用 watch;
  • 依赖是动态收集的、只是「跟着跑一次」,用 watchEffect 更短;
  • 由用户操作触发的一次性请求,不要放进 watch,写在事件处理函数里。
watch(keyword, async (value, oldValue, onCleanup) => {
    const controller = new AbortController();
    onCleanup(() => controller.abort());

    results.value = await search(value, { signal: controller.signal });
});

上面的 onCleanup 是关键:输入变化很快时,上一次请求会被中止,避免旧结果覆盖新结果。分页参数这种「两个状态都变化才请求」的场景,写成 watch([page, size], ...) 更直观。

跨组件共享状态

不需要为了一个主题开关装 Pinia。把状态放在模块作用域,导出一个组合函数就够了:

import { ref, readonly } from 'vue';

const dark = ref(false);

export function useTheme() {
    const toggle = () => { dark.value = !dark.value; };

    return { dark: readonly(dark), toggle };
}

模块级 ref 天然是单例,readonly 防止外部直接改值。涉及服务端数据缓存、跨页面持久化时再引入 Pinia,别让简单问题变复杂。

小结

状态尽量靠近使用它的组件,组合函数只做「把同一件事的代码收在一起」,不承担全局状态职责。渲染层面的性能问题属于另一类,见Vue 3 渲染卡顿排查。