一、前言
组件篇结束,新阶段开始:组合式函数(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>
能跑,但三个致命伤:
- 来源不明 :模板里用了
count、increment,但当前文件里搜不到定义------每个都得跳到 mixin 文件里确认,文件多了就是"考古" - 命名冲突 :两个 mixin 都有
increment,后者静默覆盖前者,不报错、难排查 - 数据来源错综:多个 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 的三大痛,逐条解决:
- 来源明确 :count、increment 全部来自
useCounter()的返回值,一目了然 - 无冲突 :名字冲突了会怎样?解构时自己重命名就行------
const { count: postCount } = useCounter(),主动权在使用者手里 - 可传参、可组合 :
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', '')。复用的复利,从这一刻开始计算。
【截图位置:输入草稿后刷新页面内容保留的效果】
六、踩坑记录
- 不用 use 前缀 :能跑,但全团队都会骂你------
use前缀是社区强约定(VueUse 全家都叫 useXxx),看到 useXxx 就知道"这是组合式函数,得在 setup 里同步调用" - 在异步回调里调用组合式函数 :
setTimeout(() => { const x = useCounter() })内部的生命周期钩子/inject 全部失效------组合式函数只准在 setup 同步阶段调用 - 返回值解构丢响应式 :
return { count }返回 ref 时解构没问题(ref 是盒子,012 篇);但如果内部用 reactive 聚合再直接 return,调用方解构就断------对外返回前记得 toRefs(039 篇最佳实践细讲) - 把组合式函数当组件用 :
<useCounter />❌------它是函数不是标签,作用是"给 setup 提供逻辑" - 文件位置混乱 :约定放
src/composables/(有的项目叫 hooks/),一个函数一个文件,和组件分开
七、今日小结
- mixin 三宗罪:来源不明、命名冲突、互相纠缠,已被官方边缘化
- Composables = 以 use 开头、内部用组合式 API、返回响应式数据的函数
- 有状态型每次调用独立;生命周期钩子可内含,自动绑定调用者实例
- 铁律:只在 setup 同步阶段调用 ;目录约定
src/composables/
下篇预告
理论齐了,开始动手造。第一组最简单的:useTitle (响应式改网页标题)和 useToggle(布尔开关)------十行代码的组合式函数长什么样?下一篇 036 手把手封装。