组合式 API(Composition API)

一、技术难点:为什么"按选项写组件"会崩

Options API 把组件拆成 data / methods / computed / watch / mounted 几个抽屉。组件小的时候很整齐,一旦一个功能涉及"一份状态 + 两个方法 + 一个侦听 + 一个生命周期",它的代码就被切碎扔进四个抽屉里。四处翻找改一个功能,是大型组件维护痛苦的根源。四个难点:

  1. 逻辑按"选项类型"聚合,而非按"功能"聚合。发票管理、权限校验、草稿自动保存三个功能的状态和方法全部交织在同一个 data 和 methods 里,删一个功能要小心翼翼地从各处摘除。
  2. 逻辑复用只能靠 mixin,而 mixin 是隐式契约 。多个 mixin 之间变量名冲突、来源不可追溯、this 上挂了什么只能靠运行时猜,类型推导基本失效。逻辑复用的需求真实存在,但 mixin 给了一个坏答案。
  3. setup 的心智转变 。从"在 this 上挂属性"变成"用函数闭包组织逻辑"。最典型的坑是响应式丢失 :const { count } = reactive(state) 解构出来的 count 是个普通数字,后续视图不会更新。
  4. 组合函数的边界模糊 。抽成 useXxx 之后,什么时候用 ref 什么时候用 reactive、生命周期钩子为什么必须在 setup 同步执行期注册、组合函数之间怎么通信,全是实践中的模糊地带。

一句话:组合式 API 解决的不是"语法更简洁",而是"让代码按业务功能聚合、让逻辑复用有类型可追的显式契约"。


二、完整解法:从 setup 语义到组合函数设计

2.1 先用 60 行手写一版"组合式响应式",把语义吃透

理解 Composition API 的关键是理解 ref / computed / watchEffect 三者如何靠依赖收集串起来。下面这版骨架用 Node 可直接跑,输出与 Vue 真实行为一致:

javascript 复制代码
// mini-composition.mjs ------ ref / computed / watchEffect 的最小实现
let activeEffect = null;                     // 当前正在执行的副作用
const effectStack = [];

function ref(value) {
  const dep = new Set();                     // 谁用了这个 ref
  return {
    get value() {
      if (activeEffect) dep.add(activeEffect);   // 收集依赖
      return value;
    },
    set value(v) {
      if (v === value) return;
      value = v;
      [...dep].forEach(fn => fn());              // 触发依赖
    },
  };
}

function watchEffect(fn) {
  const runner = () => { activeEffect = runner; fn(); effectStack.pop(); activeEffect = null; };
  runner();
  return runner;
}

function computed(getter) {
  const result = ref(undefined);
  watchEffect(() => { result.value = getter(); });   // 依赖变了自动重算
  return result;
}

// ---- 验证:组合式 API 的逻辑聚合 ----
const price = ref(10);       // 功能A:计价
const qty = ref(2);
const total = computed(() => price.value * qty.value);

const logs = [];
watchEffect(() => logs.push(`总价变了 → ${total.value}`));

price.value = 20;            // 触发重算与副作用
qty.value = 3;
console.log(logs);
// 输出: [ '总价变了 → 20', '总价变了 → 60' ]  ← 依赖是精确的, price 变只走一次

这 60 行揭示了组合式 API 的本质:ref 是可被追踪的容器,watchEffect 是会被自动重跑的函数,computed 是被依赖驱动的缓存值。组件里那些 setup 写法,最后都编译成这套机制。

2.2 ref / reactive 的选择与解构陷阱

场景 选择 理由
基本类型、需要整体替换 ref state.value = newObj 可整体换掉,视图照常更新
结构化对象、表单聚合 reactive 直接 form.name 访问,无需 .value
需要解构出来传参 toRefs / toRef 保住响应性,避免拆包丢失
javascript 复制代码
import { reactive, toRefs } from 'vue';
const state = reactive({ count: 0 });
const { count } = toRefs(state);     // ✅ count 仍是响应式引用
// const { count } = state;          // ❌ count 是普通数字, 视图不更新

反模式 :把 reactive 对象整个解构后再传入子函数,或把 ref 当普通值塞进闭包深处。判断标准只有一条:它是不是还能被响应系统追踪到。

2.3 组合函数:把"一个功能"收进一个函数

组合函数的写法有稳定的三段式:定义本地状态 → 定义操作与副作用 → 返回对外的引用。以"草稿自动保存"为例:

javascript 复制代码
// useAutosave.js
import { ref, watch, onScopeDispose } from 'vue';

export function useAutosave(source, save, { delay = 1000 } = {}) {
  const saving = ref(false);          // ① 本地状态
  const savedAt = ref(null);
  let timer = null;

  watch(source, () => {               // ② 操作与副作用
    saving.value = true;
    clearTimeout(timer);
    timer = setTimeout(async () => {
      await save();                   // 传给函数的保存策略, 有类型可追
      saving.value = false;
      savedAt.value = Date.now();
    }, delay);
  });

  onScopeDispose(() => clearTimeout(timer));   // 清理, 防内存泄漏

  return { saving, savedAt };         // ③ 只暴露该暴露的
}
javascript 复制代码
// 组件里按功能聚合, 而不是按选项抽屉
const { content } = toRefs(form);
const { saving, savedAt } = useAutosave(content, () => api.save(form));

对比 mixin:useAutosave 的输入输出是显式签名(谁传进、返回什么一目了然),来源清晰、可单测、类型能推导。这正是 mixin 做不到的。

2.4 两个必须知道的时序约束

  • 生命周期钩子必须在 setup 同步执行期注册 。onMounted 放在 setTimeout 或 await 之后会失效,因为它依赖当前组件实例这个隐式上下文;
  • <script setup> 是语法糖 :组件实例的创建、defineProps/defineEmits/defineExpose 这些编译宏,最终被编译成 setup() 的返回值与 props/emits 声明。理解这一层,就不会把宏当运行时函数去解构或传递。

三、应用场景

  • 复杂组件分层 :一个 800 行的表单组件,按功能抽成 useValidation / useDraft / usePermission 三个组合函数,组件本体只负责编排,可读性成倍提升;
  • 跨组件逻辑复用 :鼠标位置、请求封装、分页、埋点、abort 控制,全部收敛成 useXxx,取代 mixin 与 HOC 的隐式注入;
  • 逻辑可测:组合函数是纯函数式入口,可以在 Node 里直接单测(不必挂载组件),这正是上面手写演示的思路;
  • TS 友好 :显式输入输出签名让类型推导自然工作,配合 defineProps<T>() 泛型写法,props 类型与运行时校验统一到一处。

四、总结

组合式 API 的价值不在"写法新鲜",而在把代码组织方式从"选项抽屉"换成"功能函数":ref 是可追踪容器、computed 是依赖驱动的缓存、watchEffect 是自动重跑的副作用;组合函数用"状态 → 操作 → 返回引用"三段式收拢一个功能,并用显式签名取代 mixin 的隐式契约。三个最容易踩的坑------解构 reactive 丢响应性、生命周期钩子错过同步执行期、把编译宏当运行时函数------都源于同一件事:不理解响应系统与组件实例的隐式上下文。下一篇 Day21 进入 diff 算法,看 Vue 如何在虚拟 DOM 上把"最小的改动"算出来。

相关推荐
火柴就是我2 小时前
Android 打包报错 25.0.3
android·前端
BD_Marathon2 小时前
消息对象中字段的说明
java·前端·python
八荒启·交互动画2 小时前
Web特效025—用 Canvas 2D 做Web特效的定义与边界:这支画笔能做到哪一步
前端·webgl·网页特效·八荒启-交互动画·八荒启
Ai-_Man2 小时前
您您这可以把Dola的多个会话比如说。左侧的多个会话一次性导出吗?不是单条会话里面的多次会对话。用AI导出鸭,答案是可以的
开发语言·前端·人工智能·小程序
excel3 小时前
Nuxt 中使用 useHead 优化 SEO 与 GEO
前端
IT_陈寒4 小时前
SpringBoot启动慢得像蜗牛?原来是这个配置在捣鬼
前端·人工智能·后端
计算机魔术师4 小时前
Muse Spark跑赢Gemini,但真正的底牌是这种设计
前端
PYB34 小时前
【Web·JS·基础】函数的使用和展运算符...objs
前端·javascript
指针向南4 小时前
Chrome读不了HEIC怎么办:原生解码和WASM两条路
前端·图像处理·人工智能·chrome·计算机视觉·wasm