Vue3 零基础每日笔记(035):什么是 Composables——mixin 之死与逻辑复用新方案

一、前言

组件篇结束,新阶段开始:组合式函数(Composables)篇。本篇先回答"为什么需要它":Vue2 时代的逻辑复用方案 mixin 为什么被淘汰?Composables 凭什么成为官方钦定的答案?012 篇埋的"toRefs 在第五阶段天天见"也从这篇开始兑现。

二、先看老方案 mixin 为什么凉了

需求:计数逻辑(count + increment)要在三个页面复用。Vue2 的 mixin 写法:

js 复制代码
// src/mixins/counterMixin.js
export default {
  data() {
    return { count: 0 }
  },
  methods: {
    increment() {
      this.count++
    }
  }
}
vue 复制代码
<!-- 页面里混入 -->
<script>
import counterMixin from '@/mixins/counterMixin'

export default {
  mixins: [counterMixin],
  mounted() {
    this.increment()   // count 从哪来的?要跑去 mixin 文件查
  }
}
</script>

能跑,但三个致命伤:

  1. 来源不明 :模板里用了 count、increment,但当前文件里搜不到定义------每个都得跳到 mixin 文件里确认,文件多了就是"考古"
  2. 命名冲突 :两个 mixin 都有 increment,后者静默覆盖前者,不报错、难排查
  3. 数据来源错综:多个 mixin 之间还可能互相依赖(A 用了 B 的 data),牵一发动全身

本质问题:mixin 是"把代码复制合并进组件",合并过程黑盒且不可控。

三、Composables:函数级别的逻辑复用

组合式函数的定义简单到离谱:一个使用了 Vue 组合式 API 的普通函数,约定以 use 开头:

js 复制代码
// src/composables/useCounter.js
import { ref } from 'vue'

export function useCounter(init = 0) {
  const count = ref(init)

  function increment() {
    count.value++
  }

  function reset() {
    count.value = init
  }

  // 把响应式数据和方法"打包返回"
  return { count, increment, reset }
}
vue 复制代码
<!-- 页面使用 -->
<template>
  <p>{{ count }}</p>
  <button @click="increment">+1</button>
  <button @click="reset">归零</button>
</template>

<script setup>
import { useCounter } from '@/composables/useCounter'

// 解构即用,来源清清楚楚
const { count, increment, reset } = useCounter()
</script>

对比 mixin 的三大痛,逐条解决:

  1. 来源明确 :count、increment 全部来自 useCounter() 的返回值,一目了然
  2. 无冲突 :名字冲突了会怎样?解构时自己重命名就行------const { count: postCount } = useCounter(),主动权在使用者手里
  3. 可传参、可组合 :useCounter(10) 给初始值;组合式函数内部还能调用别的组合式函数,像搭积木一样组装逻辑

一句话定位:组合式函数把"逻辑"从"组件"里解放出来,成为独立、可测试、可复用的单元。

四、两个马上用得上的心智模型

模型 1:无状态 vs 有状态

js 复制代码
// 有状态:内部有 ref,每次调用创建**独立**的状态
const a = useCounter()  // a.count
const b = useCounter()  // b.count ------ 和 a 互不影响
js 复制代码
// 无状态(工具型):纯函数包装,不持有状态
function useAdd(a, b) {
  return computed(() => a.value + b.value)
}

大部分场景用有状态型(每次调用独立实例);需要全局共享单例时,把状态提到函数外面(模块级变量),那是 Pinia(053 篇)的主场。

模型 2:生命周期钩子可以写在里面

js 复制代码
import { onMounted, onBeforeUnmount } from 'vue'

export function useLog(name) {
  onMounted(() => console.log(`${name} 挂载了`))
  onBeforeUnmount(() => console.log(`${name} 要卸载了`))
}

组合式函数里注册的钩子会自动绑定到调用它的组件实例 上------组件卸载时自动清理。但有个铁律:必须在 setup 的同步执行阶段调用(和 028 篇 inject 同款约束),放进 setTimeout 或事件回调里钩子就绑不上实例了。

五、实战:useStorage------第一个"生产级"组合式函数

一个真正有用的例子:响应式数据 + localStorage 双向同步(010 篇草稿案例的函数化):

js 复制代码
// src/composables/useStorage.js
import { ref, watch } from 'vue'

export function useStorage(key, defaultValue = '') {
  // 初始化:优先读本地缓存
  const data = ref(JSON.parse(localStorage.getItem(key)) ?? defaultValue)

  // 数据变了就写回本地
  watch(data, (val) => {
    localStorage.setItem(key, JSON.stringify(val))
  }, { deep: true })

  return data
}
vue 复制代码
<template>
  <input v-model="draft" placeholder="草稿自动保存,刷新不丢" />
  <p>当前草稿:{{ draft }}</p>
</template>

<script setup>
import { useStorage } from '@/composables/useStorage'

// 一行顶 010 篇的一整段
const draft = useStorage('draft', '')
</script>

对比 010 篇手写的版本:组件里从"reactive + watchEffect + 存取逻辑十几行"压缩成一行 useStorage('draft', '')。复用的复利,从这一刻开始计算。

【截图位置:输入草稿后刷新页面内容保留的效果】

六、踩坑记录

  1. 不用 use 前缀 :能跑,但全团队都会骂你------use 前缀是社区强约定(VueUse 全家都叫 useXxx),看到 useXxx 就知道"这是组合式函数,得在 setup 里同步调用"
  2. 在异步回调里调用组合式函数 :setTimeout(() => { const x = useCounter() }) 内部的生命周期钩子/inject 全部失效------组合式函数只准在 setup 同步阶段调用
  3. 返回值解构丢响应式 :return { count } 返回 ref 时解构没问题(ref 是盒子,012 篇);但如果内部用 reactive 聚合再直接 return,调用方解构就断------对外返回前记得 toRefs(039 篇最佳实践细讲)
  4. 把组合式函数当组件用 :<useCounter /> ❌------它是函数不是标签,作用是"给 setup 提供逻辑"
  5. 文件位置混乱 :约定放 src/composables/(有的项目叫 hooks/),一个函数一个文件,和组件分开

七、今日小结

  • mixin 三宗罪:来源不明、命名冲突、互相纠缠,已被官方边缘化
  • Composables = 以 use 开头、内部用组合式 API、返回响应式数据的函数
  • 有状态型每次调用独立;生命周期钩子可内含,自动绑定调用者实例
  • 铁律:只在 setup 同步阶段调用 ;目录约定 src/composables/

下篇预告

理论齐了,开始动手造。第一组最简单的:useTitle (响应式改网页标题)和 useToggle(布尔开关)------十行代码的组合式函数长什么样?下一篇 036 手把手封装。

相关推荐
西凉的悲伤21 小时前
Jsoup 库 HTML 解析详解
java·前端·html·jsoup
Wanderer X1 天前
MiniMax-H3(ref2va)架构与推理方法笔记
笔记
深圳老胡1 天前
STM32C562RET6 外部中断详解:从 EXTI 原理到 GPIO 外部中断实战
笔记·stm32·单片机·代码规范·1024程序员节
海绵宝宝转agent1 天前
leetcodehot100个人题解
java·笔记·学习·算法
李某学编程2 天前
基于ARM的微机原理学习笔记(一)
arm开发·笔记·学习
sbjdhjd2 天前
AI产业周度动态:从“更强模型”走向“可执行、可治理、可计价”的系统竞争 (2026年10月19日—10月25日)
人工智能·笔记·ai·开源·gemini·信息差·claude code
inksci2 天前
在线编辑表格,功能强大,json互转
前端·javascript·html
ss2732 天前
AI时代首先数据库-PostgreSQL实战笔记 27 PostgreSQL数据写入与读取流程解析
数据库·笔记·postgresql
gc_22992 天前
使用VS Code终端创建Vue项目报错:“npm : 无法加载文件 ...\npm.ps1,因为在此系统上禁止运行脚本”
vue·vs code
深圳老胡2 天前
压力检测空吸功能硬件方案全解析:主流压力传感器品牌与实现路径
笔记·单片机·代码规范