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. 来源不明 :模板里用了 countincrement,但当前文件里搜不到定义------每个都得跳到 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 手把手封装。

相关推荐
动词ing2 小时前
【学习笔记】数据结构 二分查找(二分查找模板+边界处理+查找区间+旋转数组)
笔记·学习
kkkkkkkkkk_Z2 小时前
学嵌入式和Linux应用编程|学习日记:深入理解TCP协议与网络编程实战
linux·笔记·学习
小刘在重生~2 小时前
十六(3)、《集合扩充》CopyOnWriteArrayList 超详细解析(线程安全集合)
java·笔记·面试·职场和发展
xy34532 小时前
Axure 9.0 中继器创建与设置步骤
前端·ui·html·axure·原型·产品设计
维克兜率天2 小时前
5.3 宏观因子:抬头看天
笔记·python·深度学习·算法·量化
LuminousCPP2 小时前
数据结构-排序(五):计数排序与排序专题阶段总结|从外部归并到线性排序,再看算法边界与选型
c语言·数据结构·笔记·算法·排序算法
钓鱼的肝2 小时前
csp-j-s总结(1)
c++·笔记·算法·青少年编程
The Chosen One9852 小时前
OS第二章随手记(2.1)
linux·运维·服务器·笔记
志尊宝3 小时前
Vue3 零基础每日笔记(030):动态组件与 keep-alive——切换组件与页面缓存
笔记·vue·html·前端开发·软件开发