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 的组合式 API(Composition 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选项。但在同一组件里把自己写成setup+data+methods,会让代码焦点分裂,不方便后续维护。如果你锁定 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 的省事写法。