讲解

组合式函数(composable)是 Vue 3 逻辑复用的标准答案:一个以 use 开头的普通函数,内部使用 ref/computed/watch 等组合式 API,返回需要的状态和方法。组件之间重复的「带状态逻辑」——倒计时、鼠标位置、请求封装、本地存储同步——都抽成 composable。和高阶组件、mixin 相比,它的优势是:数据来源清晰(返回值一目了然)、没有命名冲突、天然支持类型推导。

写 composable 有三条约定。命名以 use 开头(useCounter、useFetch),一眼可辨。输入灵活:参数既接受普通值也接受 ref,内部用 toValue/unref 归一化,调用方传什么都行。返回对象:返回一个 plain object 让使用方自由解构,需要时配合 toRefs 保持响应式。

composable 的威力在于「状态跟着函数走」。同一个 useCounter 在两个组件里调用,各自有独立的 count——因为它内部每次调用都创建新的 ref。而如果想做全局共享状态,把 ref 提到函数外面(模块作用域)即可——这也是手写简易状态管理的原理。组合式函数可以互相嵌套(useFetch 内部用 useLoading),复杂逻辑就这样一层层搭出来。

示例

实现两个真实可用的 composable(纯逻辑版):useCounter 和 useLocalStorage 的存取协议(在本教程构建时被真实执行):

import assert from 'node:assert/strict';

// 迷你 ref
function ref(v) {
  return { value: v };
}

// composable 1:useCounter —— 每次调用独立状态
function useCounter(initial = 0) {
  const count = ref(initial);
  return {
    count,
    inc: () => count.value++,
    dec: () => count.value--,
    reset: () => (count.value = initial),
  };
}

const a = useCounter(0);
const b = useCounter(10);
a.inc();
a.inc();
b.dec();
assert.strictEqual(a.count.value, 2); // 互不影响
assert.strictEqual(b.count.value, 9);

// composable 2:useStorage —— 状态与 localStorage 同步
const memoryStore = new Map(); // 模拟 localStorage
function useStorage(key, initial) {
  const saved = memoryStore.get(key);
  const data = ref(saved !== undefined ? JSON.parse(saved) : initial);
  return {
    data,
    save() {
      memoryStore.set(key, JSON.stringify(data.value)); // 真实版本用 watch 自动同步
    },
  };
}

const prefs = useStorage('prefs', { theme: 'light' });
prefs.data.value.theme = 'dark';
prefs.save();
const prefs2 = useStorage('prefs', { theme: 'light' }); // 刷新页面后重读
assert.strictEqual(prefs2.data.value.theme, 'dark');

console.log('两个 useCounter 实例:', a.count.value, '和', b.count.value, '(状态独立)');
console.log('刷新后从存储恢复的主题:', prefs2.data.value.theme);

组件中使用 composable:

<script setup>
import { useCounter } from './composables/useCounter';
import { useFetch } from './composables/useFetch';

const { count, inc, reset } = useCounter(0);
const { data, loading } = useFetch('/api/todos');
</script>

<template>
  <button @click="inc">{{ count }}</button>
  <button @click="reset">归零</button>
  <p v-if="loading">加载中…</p>
  <pre v-else>{{ data }}</pre>
</template>
// composables/useFetch.js —— 仅示意:真实项目代码(依赖 vue 包),构建时不执行
import { ref, toValue, watchEffect } from 'vue';

export function useFetch(url) {
  const data = ref(null);
  const loading = ref(false);
  const error = ref(null);

  watchEffect(async () => {
    loading.value = true;
    error.value = null;
    try {
      const res = await fetch(toValue(url)); // 支持传 ref
      data.value = await res.json();
    } catch (e) {
      error.value = e;
    } finally {
      loading.value = false;
    }
  });

  return { data, loading, error };
}

常见坑

  • 在 composable 里用 this:Composition API 没有组件实例上下文,写 this 的地方都该重新设计。
  • 返回 reactive 对象被解构:解构 reactive 丢响应式,返回 ref 组合或让调用方 toRefs。
  • 忘记处理参数是 ref 还是普通值:库级 composable 用 toValue 归一化,业务内的简单场景可以约定死。
  • 在 composable 里直接操作 DOM:DOM 逻辑属于指令或组件,composable 管状态和副作用。
  • 模块级 ref 误用:想要共享状态才提到模块作用域,否则每次调用该创建新状态。

小结

composable = use 开头的函数 + 组合式 API + 返回状态/方法;实例独立靠函数内 ref,全局共享靠模块级 ref。下一章讲跨层级通信:provide/inject。