1. 先搞清楚 Mixin 到底在解决什么Mixin 在 Vue 社区里一直是个让人又爱又恨的特性。爱它的团队觉得它能轻松把请求逻辑、分页逻辑、表单校验逻辑从组件里抽出来几行代码注入进去就完事恨它的团队基本都经历过排查一个来源不明的this.total字段时在十几个 Mixin 里来回翻文件的惨痛教训。先说个结论Mixin 这套机制本质上是Vue2 时代为“跨组件复用可响应状态与生命周期逻辑”设计的最轻量方案。在 Vue2 里如果你不想写一个完整的状态管理库比如 Vuex又确实有多个组件需要共享同一套“初始化数据、拉取接口、处理加载状态”的逻辑Mixin 是当时语法成本最低的选择。它的核心机制用一句话就能讲透把一个对象里的 data、methods、computed、watch、生命周期钩子按规则“揉进”目标组件的选项对象里。Vue 在组件实例化时会先解析 Mixin 选项再解析组件自身的选项里面的合并策略会让不同的字段产生不同的合并效果。所以Vue2 项目里最典型的 Mixin 应用场景就是这三类页面级的通用逻辑抽取比如列表页都要做搜索、分页、重置请求中要显示 loading与生命周期绑定的副作用逻辑比如进入页面要刷新数据、离开页面要清掉定时器/事件监听跨组件的“非状态工具方法集合”比如日期格式化、金额四舍五入、路由跳转封装挂在methods里各处调用。这个设计在 Vue2 时代看起来没毛病。但真正接手过两三个大量使用 Mixin 的中大型项目就会清楚它身上有几个很难根治的缺点。要想把 Vue3 的组合式 APIComposition API用明白第一步反而是把 Mixin 的来龙去脉给它吃透。1.1 把 Mixin 的合并规则先背下来很多教程会机械地告诉你 Mixin 有哪些合并策略但没讲清楚为什么这样设计。我试着用自己的理解还原一下设计者的思路data 和 methods 是组件自己的“私有属性”组件写的应该优先于外部混入的生命周期钩子则是一组“随实例一起运行的任务”任务之间不该互相覆盖所以选择按顺序全部执行。具体到选项上Vue2/Vue3选项式写法的合并规则如下选项类型合并行为优先级data返回值中的字段组件字段与 Mixin 字段各自保留同名冲突时组件覆盖 Mixin组件优先methods中的方法同名冲突时组件覆盖 Mixin组件优先computed中的计算属性同名冲突时组件覆盖 Mixin组件优先watch同名监听可以共存回调都会执行Vue3 中可通过flush配置顺序多源并存生命周期钩子全部按顺序执行Mixin 的钩子先执行组件自身的后执行多源并存components、directives、filters注册项合并同名时组件优先组件优先这里有一个很多人会忽略的细节生命周期钩子的执行顺序是 Mixin 先跑组件后跑。这意味着如果某个逻辑必须等组件自身的data完全初始化之后再执行你在 Mixin 的created里做初始化操作是安全的因为此时所有 data 字段都已代理到实例上。反之如果你在 Mixin 的created里要调用组件自身的某个methods方法大部分情况下也成立因为 methods 和 data 的初始化都在created之前完成。理解这个顺序是排查很多诡异问题的第一把钥匙。1.2 Vue2 项目里最常抽 Mixin 的三类场景第一类是列表页通用逻辑。几乎所有后台管理系统80% 的页面都是“上方筛选表单 中间表格 下方分页”。一个团队如果人手不够很容易沉淀出类似listMixin的东西内部维护page、pageSize、total、list、loading这些字段同时暴露fetchList、handleSearch、handleReset、handlePageChange这些方法。后来新写的页面只要mixins: [listMixin]再重写fetchList的拉取逻辑即可。第二类是带生命周期副作用的逻辑。比如一个竞品数据看板页面需要每隔 30 秒轮询一次接口一个富文本编辑器页面离开前要销毁编辑器实例、移除 resize 监听。这类代码如果不抽 Mixin就得在每个组件的mounted和beforeUnmount里各写一遍非常容易漏掉清理动作导致内存泄漏。抽成 Mixin 之后把setInterval的创建和销毁收敛到一个地方至少能保证逻辑只写一次。第三类是毫无状态的方法集合。这类场景其实就是把 Mixin 当工具函数使比如把formatDate、debounce、deepClone挂到一个叫utilMixin的对象里组件通过this.utilMixin去调用。我个人其实不太建议把无状态方法挂进 Mixin因为 Mixin 的字段会混入组件的this顶层命名空间污染特别严重我更倾向于挂到Vue.prototype上Vue2或者直接抽成独立的工具模块按需 import 即可。2. Mixin 用得越深问题暴露得越明显如果只是在小项目少量场景用 Mixin几乎感觉不到它的毒性。但当一个项目的 Mixin 数量超过三四个或者 Mixin 本身也有了层级依赖比如一个 Mixin 内部mixins: [baseMixin]之后就会陆续遇到几个非常头疼的问题。2.1 隐式依赖让你根本查不到字段从哪来想象一个场景拿到一个历史遗留组件里面模板上用了一个this.currentRow。你试图在组件的 data、methods、computed 里找currentRow的定义结果发现根本没有。最后全项目全局搜索才发现它来自一个叫rowSelectMixin的文件而该 Mixin 又被另一个tableBaseMixin通过mixins选项间接引入。这不是段子是真实经历。Mixin 最大的问题就是隐式依赖组件与 Mixin 之间是一种弱契约关系组件没有显式声明“我使用了来自某个 Mixin 的某个字段”导致可读性断崖式下降。在多人协作项目里这种代码的维护成本会随时间线性增长。每次改动字段前都得先确认它还有没有其他 Mixin 在引用相当于把简单问题复杂化。2.2 命名冲突一直存在只是你没踩到而已Mixin 合并策略表里data、methods、computed都是组件优先。这听起来像是“组件赢了没事”但这个规则恰恰会让冲突在没有任何警告的情况下悄悄发生。举例来说某个 Mixin 内部定义了data() { return { loading: false } }用于组件内某个异步流程的加载态。结果你写的组件自身也有一个loading本意是控制某个按钮的 loading。因为组件优先页面跑起来以后按钮 loading 是正常的但某天你删掉了组件自身的loading字段按钮的 loading 就莫名其妙绑到了 Mixin 的loading上行为瞬间错乱。这种问题在开发期几乎不会报错只能靠代码评审或踩线上 bug 才能发现。命名冲突是 Mixin 的定时炸弹。2.3 一个组件混入多个 Mixin 时状态线程变得难以推理假设组件有mixins: [userMixin, listMixin, dialogMixin]这三个 Mixin 都在created里做了初始化操作。执行顺序看起来很简单先按数组顺序执行所有 Mixin 的 created再执行组件自身的 created。但当userMixin.created里访问了listMixin里的 data 字段时问题就来了——你无法直观地看到依赖关系。更夸张的是如果某个字段同时被三个 Mixin 修改调试时你根本不知道是哪个 Mixin 在什么时候动了它。这种“状态来源不确定”的问题才是 Mixin 让大型项目失控的核心原因。2.4 Mixin 之间无法直接沟通只能通过共享实例状态Mixin 的复用单元是“对象片段”它没有自己的独立实例。想实现 Mixin A 调用 Mixin B 的方法没有特别优雅的办法往往只能通过this上的共享字段来传递本质上依赖的还是那套命名约定。时间一长代码里就会散落一堆“只有约定、没有约束”的隐式依赖。3. 为什么说组合式 API 能覆盖 Mixin 几乎全部的场景Composition API 在 Vue3 中引入的主题思想是把“按选项类型来组织代码”改为“按逻辑关注点来组织代码”。它提供了setup函数作为组件逻辑的编排入口并且允许你自由定义组合式函数useXxx。组合式函数本质上是纯函数 Vue 响应式 API 的组合优点非常直接。如果要用一句话概括它与 Mixin 的核心差别Mixin 是“注入式的代码合并”组合式函数是“显式调用的逻辑组合”。组合式函数能通过显式返回的方式明确告诉你这个函数提供了哪些状态、哪些方法。组件接收时也可以用完全不受约束的变量名去接收// 用组合式函数改写 listMixin // composables/usePaginationList.js import { ref, computed } from vue export function usePaginationList(fetchApi, initialParams {}) { const page ref(1) const pageSize ref(10) const total ref(0) const list ref([]) const loading ref(false) const params ref(initialParams) const fetchList async () { loading.value true try { const data await fetchApi({ page: page.value, pageSize: pageSize.value, ...params.value }) list.value data.list total.value data.total } finally { loading.value false } } const handleSearch () { page.value 1 fetchList() } const handleReset () { params.value {} page.value 1 fetchList() } const handlePageChange (nextPage) { page.value nextPage fetchList() } return { page, pageSize, total, list, loading, fetchList, handleSearch, handleReset, handlePageChange } }在组件里使用script setup import { reactive } from vue import { usePaginationList } from /composables/usePaginationList import { fetchOrderList } from /api/order const { page, pageSize, total, list, loading, handleSearch, handleReset, handlePageChange } usePaginationList(fetchOrderList, reactive({ status: })) handleSearch() /script对比一下就能看出来组合式函数把“返回了哪些状态”摆在了明面上组件自身甚至可以重命名后接收完全规避了命名冲突。3.1 看一个真实场景的拆分对比带自动清理的定时器逻辑Mixin 写法通常是这样// mixins/intervalMixin.js export default { data() { return { timer: null } }, mounted() { this.startInterval() }, beforeUnmount() { this.clearInterval() }, methods: { startInterval() { this.timer setInterval(() { this.onIntervalTick this.onIntervalTick() }, 3000) }, clearAndRestart() { clearInterval(this.timer) this.startInterval() }, clearInterval() { if (this.timer) { clearInterval(this.timer) this.timer null } } } }组件里引入它时必须在组件内部额外定义onIntervalTick方法否则定时器虽然启动了但没有任何逻辑执行。这是一种反向约束Mixin 里可能调用组件方法但组件是否定义了该方法Mixin 根本不关心只会在运行时报undefined调用错误。如果用组合式函数写法可以把“需要定时执行的回调函数”直接作为参数传入把它变成一种纯逻辑的组合// composables/useInterval.js import { onMounted, onUnmounted } from vue export function useInterval(callback, interval 3000) { let timer null const start () { if (timer) return timer setInterval(callback, interval) } const clear () { if (timer) { clearInterval(timer) timer null } } onMounted(start) onUnmounted(clear) return { start, clear } }组件里使用时回调逻辑直接写在调用处谁在用这个 interval、执行逻辑是什么一目了然script setup import { useInterval } from /composables/useInterval import { ref } from vue const count ref(0) const { start, clear } useInterval(() { count.value getLatestCount() }, 5000) // 某些业务需要手动重启时再直接调用 start/clear /script从这个案例就能理解组合式函数为什么更“香”依赖总是从参数传入生命周期钩子由函数内部注册调用方不需要关心清理细节而返回的方法又会返回给调用方用于手动控制。3.2 生命周期执行顺序的秘密为什么自动清理这么顺畅这里有必要展开一下组合式 API 内部的一个实现细节。很多新手容易忽略在setup中调用onMounted、onUnmounted这些生命周期注册函数时Vue 内部会把它注册到当前正在初始化的组件实例上并自动与组件实例绑定。也就是说只要你在 setup 里调用了onUnmounted当组件销毁时注册的回调就会被批量执行。这就解决了一个 Mixin 里特别的麻烦事你以前必须在mounted手动开启定时器再在beforeUnmount里手动清理两边代码隔得很远容易漏。而组合式函数把onMounted和onUnmounted写在同一个函数内部、同一屏代码上职责天然聚合。从源码实现上说注册阶段只是往组件实例的[lifecycle]队列里 push 回调跟选项式生命周期里调用会有微妙的时机差异但绝大多数业务场景不需要关注那点先后。你只需要记住一个结论在组合式函数内部注册的生命周期钩子会在宿主组件对应阶段自动执行不需要额外手动挂载。4. 三种高频业务场景的替换示例有了前面的基础接下来我们扎进具体场景看看常见的 Mixin 会怎样被替换成组合式函数。4.1 场景一列表页搜索、分页、重置逻辑这是后台管理系统最常遇到的情况。旧代码如果是基于 mixins 的fetchListMixin和searchMixin拆分出来通常是这样的逻辑流程初始化created里调一次fetchList拿到首次数据搜索把表单数据写入queryParams重置page 1再请求接口分页修改page和pageSize重新请求接口重置清空查询条件page 1重新请求。使用组合式函数后的通用实现可以参考第 2 节里的usePaginationList但在实际项目中接口的数据结构不统一可能有rows、records、list等不同字段也可能后端返回的直接就是数组。因此建议带着一个可选的解析器函数参数去封装// composables/useListPage.js import { ref } from vue export function useListPage(fetcher, options {}) { const { defaultParams {}, listKey list, transform (res) res?.data ?? res } options const loading ref(false) const list ref([]) const total ref(0) const page ref(1) const pageSize ref(defaultParams.pageSize || 10) const params ref({ ...defaultParams }) const fetchData async () { loading.value true try { const res await fetcher({ page: page.value, pageSize: pageSize.value, ...params.value }) const result transform(res) list.value result[listKey] || [] total.value result.total || 0 } finally { loading.value false } } const onSearch () { page.value 1 fetchData() } const onReset () { params.value { ...defaultParams } page.value 1 fetchData() } const onPageChange (p) { page.value p fetchData() } return { loading, list, total, page, pageSize, params, fetchData, onSearch, onReset, onPageChange } }调用的组件会变成这样script setup import { reactive } from vue import { useListPage } from /composables/useListPage import { queryGoodsList } from /api/goods const formModel reactive({ keyword: , category: }) const { loading, list, total, page, pageSize, fetchData, onSearch, onReset, onPageChange } useListPage( (p) queryGoodsList({ ...p, ...formModel }), { defaultParams: { keyword: , category: }, listKey: records, transform: (res) res } ) onSearch() /script你会发现配合reactive表单对象组件自己的筛选条件和组合式函数内部的请求参数完全解耦而且模板上绑定的page、list、total依然是响应式的分页组件可以直接用。4.2 场景二远程下拉框的搜索与防抖逻辑做后台的人肯定写过“远程搜索下拉框”的需求输入关键词防抖后调用接口把选项渲染出来选中后回填默认值。这个逻辑用 Mixin 写的话通常是封一个remoteSelectMixin里面维护remoteOptions、remoteLoading、remoteSearch等方法。但在多级联动的表单里不同下拉框的接口参数不一样Mixin 很难通过配置完全覆盖组合式函数则可以轻松做到// composables/useRemoteSelect.js import { ref, watch } from vue import { debounce } from lodash-es export function useRemoteSelect(fetcher, { immediate false, debounceMs 300 } {}) { const options ref([]) const loading ref(false) const keyword ref() const loadOptions async (kw) { if (loading.value) return loading.value true try { const result await fetcher(kw) options.value result || [] } finally { loading.value false } } const debouncedLoad debounce((kw) loadOptions(kw), debounceMs) watch(keyword, (val) { debouncedLoad(val) }) if (immediate) { loadOptions() } return { options, loading, keyword, reload: () loadOptions(keyword.value) } }在组件里用的时候每个下拉框都是独立的一个useRemoteSelect调用互不干扰参数也可以各不相同。这种组合方式在多级联动下会非常舒服script setup import { watch } from vue import { useRemoteSelect } from /composables/useRemoteSelect import { fetchProvinces, fetchCities } from /api/region const provinceQuery useRemoteSelect(fetchProvinces, { immediate: true }) const cityQuery useRemoteSelect((pid) fetchCities(pid), { debounceMs: 200 }) watch( () provinceQuery.value, (val) { cityQuery.keyword val cityQuery.reload(val.id) } ) /script上面的代码不再需要把一个联动逻辑拆到多个 Mixin 里因为每个下拉框的独立状态都在它自己的组合式函数实例里相互之间的数据传递就是普通变量。4.3 场景三表单校验与提交状态管理很多后台页面带有详情表单进入页面要先拉详情、回填到表单表单有编辑/查看模式提交前校验提交中有 loading提交成功后根据不同的返回码做处理。如果把这些全部塞进一个formMixin会导致 Mixin 的 props 越来越复杂。拆成组合式函数可以更细粒度地管理// composables/useFormDetail.js import { reactive, ref, toRaw } from vue export function useFormDetail({ detailApi, saveApi, rules, transformDetail }) { const form reactive({}) const loading ref(false) const submitting ref(false) const isEdit ref(false) const loadDetail async (id) { loading.value true try { const detail await detailApi(id) Object.assign(form, transformDetail ? transformDetail(detail) : detail) isEdit.value true } finally { loading.value false } } const validate () { for (const key of Object.keys(rules)) { const rule rules[key] const value toRaw(form)[key] const err typeof rule function ? rule(value, form) : undefined if (err) { // 这里可对接 UI 框架的校验展示例如 el-form 的 validate return Promise.reject(new Error(err)) } } return Promise.resolve() } const submit async () { await validate() submitting.value true try { await saveApi({ ...toRaw(form) }) return true } finally { submitting.value false } } return { form, loading, submitting, isEdit, loadDetail, validate, submit } }其实在实际业务里你完全不需要彻底套用一个完美的封装只需要把“这段逻辑要在很多组件里用”的部分抽成组合式函数其他部分留在组件里就已经获得了比 Mixin 好得多的体验。5. 从多个维度对比 Mixin 和组合式 API 的替代关系这里我把两者放到一张全景表里方便以后在技术评审时用来判断“该不该用一个组合式函数去替换现存 Mixin”。对比维度Mixin组合式 API / 组合式函数逻辑组织方式按选项data/methods/生命周期切分按业务关注点切分每个函数内部同时包含状态与方法依赖来源隐式依赖组件内同名属性或方法显式依赖参数直接传入命名冲突同名覆盖无警告排查成本很高状态接收时可重命名天然规避冲突可测试性需要挂载真实组件才能测试可以直接将组合式函数当作普通函数编写单元测试复用粒度一段选项片段一个独立闭包/函数实例逻辑间通信需要通过共享实例状态的约定完成通过参数传递或组合函数返回值完成生命周期不能延迟注册只能在选项上声明可以在函数内部调用onMounted等且会跟随组件生命周期自动注册对 Tree Shaking 或代码提示支持较差较好可用 TypeScript 推导完整类型迁移成本Vue3 仍兼容选项式 Mixin但因为行为一致所以旧问题也在一次迁移需要重构组件编写思路但收益稳定这张表仅供参考。项目里实际处理时我通常遵循一个非常务实的替换策略如果你只有 1-2 个非常稳定的 Mixin并且团队心智一致短期内不重构也不是不行如果 Mixin 开始嵌套、有跨 Mixin 的字段访问或者每个新需求都要小心翼翼检查“有没有覆盖别人家字段”那就该拆了。新代码一律用组合式函数存量代码按“高频变更模块”优先替换不用强求一次性全量迁移。6. 组合式 API 在替换 Mixin 路上的几个隐蔽细节理论和例子都讲了不少但实际替换过程中有几个细节特别容易让老 Vue2 开发者在 Vue3 里踩坑。我根据自己的项目经验把这些问题集中整理在下面。6.1 组合式函数里“状态该用 ref 还是 reactive”要提前统一很多从 Mixin 切到组合式函数的人容易在ref和reactive之间摇摆不定。我的经验是在组合式函数内部默认用ref保存基础类型、数组、以及需要结构返回的值如果是一组强关联的对象字段用reactive更顺手但注意它不能被直接结构后再修改否则会丢失响应性。例如在usePaginationList中我用ref保存list、total、page是因为这些状态会作为返回值被解构后暴露给模板。如果用reactive包裹了整组状态再结构返回那page和pageSize就全断开了响应式连接哪怕模板还叫page也不会更新。如果想用reactive风格要么返回时用toRefs要么始终通过.value访问。我建议团队在开始时就把约定定死组合函数内部状态统一优先ref需要整块传参的对象才用reactive。如果写成reactive外部使用时必须显式写state.value或多调用一次toRefs。6.2 生命周期注册必须在 setup 执行阶段同步调用在组合式函数里调用onMounted、onUnmounted有一个需要注意的限制这些注册函数必须在组件setup执行阶段的同步代码中调用。换句话说你不能在异步回调里调用onMounted比如// 错误示例 export function useWrongDemo() { setTimeout(() { onMounted(() { console.log(永远不会按预期执行) }) }, 100) }Vue 内部通过一个全局的“当前实例”来实现onMounted的注册异步回调执行时这个“当前实例”上下文早就不在了注册就会失效或直接报错。这个限制在 Mixin 里不存在因为 Mixin 的钩子是选项静态声明的不存在“动态注册”或“调用时机”问题。所谓“组合式函数必须同步调用生命周期注册”本质上是组合式 API 这把刀最需要避开的雷区。6.3setup中拿不到this但也没必要拿很多 Vue2 老手初写 Vue3 时会下意识在setup里想用this.$router、this.$route、this.$store结果发现是undefined。这是一个必须跨过去的心智门槛。setup的执行发生在实例创建早期不能通过this访问组件实例取而代之的是需要用 Vue3 提供的组合式 API 或者从应用实例上获取import { useRouter, useRoute } from vue-router import { useStore } from vuex // 或 pinia const router useRouter() const route useRoute()这里的useRouter、useStore本身也是组合式函数。也就是说生态里的这些工具函数和你的业务组合式函数保持同一套组合逻辑。这比 Mixin 时代到处this.$xx要清晰得多。6.4 组合式函数之间可以互相调用但注意别制造地狱层级组合式函数之间互相调用是支持且推荐的。比如上面useRemoteSelect可以在内部调用useRequest一个通用的请求状态管理函数这比 Mixin 之间互相依赖要干净得多因为依赖会通过参数显式传递。但也要注意别造出太深的函数调用链。如果一个页面里一个usePage引了七八个深层组合函数每个又配置了大量 option那调试时的阅读成本同样不低。我的习惯是控制在 2-3 层以内最外层组合式函数就像页面级控制器负责编排内部更细粒度的函数再深的话就考虑页面是否被拆得太碎了。7. 常见问题与排查技巧实录到了这一部分我以问答形式把这几年在 Mixin 迁移和组合式 API 实践中遇到的高频问题整理成一个速查表方便你直接套用排查思路。7.1 已经存在大量 Mixin 的 Vue2 项目怎么平滑迁移到组合式 API不建议直接重构大 Mixin。先用“功能边界”来评估按业务模块把页面列出来找出变更频率最高的 20% 页面优先迁移。迁移单个页面时逐个分析它用到了哪些 Mixin 里的字段和方法挑出真正只用在该页面的部分内联成组合式函数其他页面也在用的情况下再把组合式函数提到composables/目录共享。这个渐进式过程基本不会打断已有业务。7.2 Vue3 选项式 API 里还能用 Mixin 吗能不能混用能。Vue3 依然兼容选项式 API也依然支持mixins选项。但在同一组件里把自己写成setupdatamethods会让代码焦点分裂不方便后续维护。如果你锁定 Vue3 选项式写法希望继续用 Mixin那至少要在团队规范里限定每个组件的 Mixin 数量不超过 2 个并且 Mixin 内部不要访问组件的私有方法。然而中长期看组合式 API 对类型推导和 SSR 更友好建议逐步迁到组合式函数和script setup上。7.3 组合式函数里返回一个reactive对象外部想要单独更新某个字段应该怎么办可以直接通过state.xxx ...更新因为它是代理对象。但当组件里把它结构出来时要处理响应式丢失const { name, age } toRefs(state)如果只是想在模板上用结构后直接把整个state返回给模板去绑定是最简单的。如果是为了在事件处理函数里更新单个属性直接用state.xxx即可不要试图对已解构出的普通变量赋值。7.4 用组合式函数替换完 Mixin 后组件的watch和computed会有变化吗会有少部分变化。在script setup里computed和watch都是直接从vue包引入的 API不再依赖选项式声明script setup import { ref, computed, watch } from vue const page ref(1) const total ref(0) const maxPage computed(() Math.max(1, Math.ceil(total.value / 10))) watch([page, total], ([newPage, newTotal]) { console.log(page or total changed, newPage, newTotal) }) /script习惯 Mixin 里声明computed和watch的写法后切换到组合式函数时注意所有watch要显式传入监听源。如果需要监听一个对象的深层变化还要追加{ deep: true }这点和 Vue2 的watch默认行为不完全一样。8. 最后一个很实用的建议把组合式函数当作团队主推的标准复用单元从我个人的实践经验来看Vue3 项目里真正要淘汰的不只是 Mixin 这个关键字而是**“把复用逻辑硬塞进组件实例”**的思考模式。一旦把“组合式函数”定为团队标准代码的组织方式就会从“这个页面要混入哪些能力”转变成“这个页面依赖了哪些逻辑块”心智负担明显下降review 也能更集中于业务细节。最后分享一个团队落地时特别好用的规矩新增可复用逻辑时任何方案都要先问一句——如果这个需求没有 Vue 运行时我应该写一个什么样的纯 JavaScript 模块把逻辑与组件解耦组合式函数就是这种思路的自然产物。只要紧握这个原则你几乎不会再去羡慕 Mixin 的省事写法。