1 vue
1.1 v-if
00.总结
a.v-once
定义它的元素或组件只染一次包括元素或组件的所有子节点首次渲染后不再随数据的变化重新渲染将被视为静态内容
b.v-cloak
这个指令保持在元素上直到关联实例结束编译,解决初始化慢导致页面闪动的最佳实践
c.v-bind
绑定属性,动态更新HTML元素上的属性,例如v-bind:class
d.v-on
用于监听DoM事件例如v-on:click v-on:keyup
e.v-html
赋值就是变量innerHTML,注意防止xss攻击
f.v-text
更新元素的textContent
g.v-model
在普通标签:变成value和input的语法糖,并且会处理拼音输入法的问题
在组件上面:处理为value和input语法糖
h.v-if/v-else/v-else-if
可以配合template来使用
在render函数里面就是三元表达式
i.v-show
使用指令来实现,最终会通过display来进行显隐
j.v-for
循环指令编译出来的结果是_l代表渲染列表
优先级比v-if高最好不要一起使用,尽量使用计算属性去解决
注意增加唯一key值,不要使用index作为key
k.v-pre
跳过这个元素以及子元素的编译过程,以此来加快整个项目的编译速度
01.v-if、v-show
a.编译
v-if 在编译过程中会被【转化成三元表达式】,条件不满足时不渲染此节点
v-show 会被编译成【指令】,条件不满足时控制样式将对应节点隐藏 (display:none)
b.场景
v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景
v-show 适用于需要非常频繁切换条件的场景
02.v-if与v-for为什么不建议一起使用
因为解析时,【先解析 v-for 再解析 v-if】,如果遇到需要同时使用时,可以考虑写成计算属性的方式
03.v-for为什么要加key
通过【提供key】,Vue.js可以在【数组变化】时,【更准确地识别、定位和更新元素】
这样就可以正确地处理新元素的添加、元素的重新排序和元素的删除
04.v-for为什么不建议用 index 作为循环项的 key 呢?
如果我们以循环的索引作为key,这些节点的key 值也会随之发生变化,导致它们被识别为新创建的节点,而不是仅更新相应的内容。
这将导致整个列表的重新渲染,即使实际上只有一个节点发生了变化。
因此,为了避免以上问题,我们不建议使用循环的索引作为节点的key。
1.2 mvvm
01.回答
View <--双向变化-> Model
View的变化能实时让Model发生变化,而Model的变化也能实时更新View
02.说明
Vue数据双向绑定原理是通过 数据劫持结合发布者-订阅者模式 的方式来实现的
首先是通过 ES5 提供的 Object.defineProperty() 方法来劫持(监听)各属性的 getter、setter
并在当监听的属性发生变动时通知订阅者,是否需要更新,若更新就会执行对应的更新函数
03.工作原理
a.ViewModel层
做了两件事达到了数据的双向绑定 一是将【模型】转化成【视图】,即将后端传递的数据转化成所看到的页面
实现方式:数据绑定。二是将【视图】转化成【模型】,即将所看到的页面转化成后端的数据
实现方式:DOM 事件监听
b.MVVM与MVC最大的区别
它实现了 View 和 Model 的自动同步
也就是当 Model 的属性改变时,我们不用再自己手动操作 Dom 元素,来改变 View 的显示
而是改变属性后该属性对应 View 层显示会自动改变(对应Vue数据驱动的思想)
c.MVVM比MVC精简很多
不仅简化了业务与界面的依赖,还解决了数据频繁更新的问题,不用再用选择器操作 DOM 元素
因为在 MVVM 中,View 不知道 Model 的存在,Model 和 ViewModel 也观察不到 View
这种低耦合模式提高代码的可重用性
1.3 生命周期
00.汇总
a.核心
a.原理
01.创建阶段 -> 02.挂载阶段 -> 03.运行阶段 -> 04.销毁阶段
一个对象从出生→长大→工作→死亡"的完整过程,每个阶段都有对应的钩子让你插入代码
b.变化
vue2最常用的3个:created → mounted → beforeDestroy
vue3取消了beforeCreate和created,用setup()函数替代了!
-------------------------------------------------------------------------------------------------
Vue3用setup()替代了beforeCreate/created,用on前缀钩子替代了原钩子,推荐用Composition API(<script setup> + onXxx()),比Vue2更简洁、更强大、更易维护。
c.过程
加载渲染过程 父 beforeCreate->父 created->父 beforeMount->子 beforeCreate->子 created->子 beforeMount->子 mounted->父 mounted
子组件更新过程 父 beforeUpdate->子 beforeUpdate->子 updated->父 updated
父组件更新过程 父 beforeUpdate->父 updated
销毁过程 父 beforeDestroy->子 beforeDestroy->子 destroyed->父 destroyed
b.vue2
a.快速参考:我该用哪个钩子?
问题 → 答案
1."我要发起API请求加载数据" → created() ✅
2."我要操作DOM或调用第三方库" → mounted() ✅
3."我要监听data变化更新DOM" → watch或computed(不是钩子)
4."我要在卸载前清理东西" → beforeDestroy() ✅
5."我需要在所有子组件加载完后做事" → mounted() ✅
6."我想在更新后做检查" → updated()(按需)
7."我需要访问data但组件还未挂载到页面" → created() ✅
8."我需要修改DOM样式或内容" → mounted() 或 updated()
b.思维导图速记
Vue2生命周期 = 对象的完整生命过程
初始化 → 挂载 → 运行 → 销毁
beforeCreate beforeMount beforeUpdate beforeDestroy
↓ ↓ ↓ ↓
data/methods未准备 DOM即将插入 data变化未渲染 即将销毁清理资源
created ✅ mounted ✅ updated ✅ (destroyed少用)
↓ ↓ ↓
初始化数据 DOM操作/库初始化 监测DOM结果 清理定时器/事件
发起请求 第三方库初始
-------------------------------------------------------------------------------------------------
最常用的3个:created → mounted → beforeDestroy
c.vue3
a.快速查询:我该用哪个钩子?
需求 使用钩子 代码位置
初始化数据、发起API onMounted onMounted
操作DOM、init库 onMounted onMounted
监听数据变化 watch setup 中
获取更新后的DOM nextTick() onMounted 或 watch
清理定时器、事件 onBeforeUnmount onBeforeUnmount
缓存组件激活 onActivated onActivated
销毁库实例 onBeforeUnmount onBeforeUnmount
b.Vue3 vs Vue2 的设计思想区别
角度 Vue2 Vue3
核心编程 Options API(选项式) Composition API(组合式)
钩子方式 对象属性 函数调用
代码组织 逻辑分散到各钩子 逻辑聚在setup中
类型推导 较差 优秀(TypeScript友好)
代码复用 mixins(有问题) Composables(推荐)
何时学 必须学 强烈推荐,Vue3新项目首选
c.Vue2 vs Vue3 快速对比表
阶段 Vue2 Vue3 Composition API Vue3 Options API 变化说明
创建阶段 beforeCreate 直接写 setup() ❌ 无 完全变了,没有这俩钩子了
created 直接写 setup() ❌ 无
挂载阶段 beforeMount onBeforeMount beforeMount 加on前缀更直观
mounted onMounted ✅ mounted 最常用
更新阶段 beforeUpdate onBeforeUpdate beforeUpdate 逻辑不变,名字改了
updated onUpdated updated 常用
销毁阶段 beforeDestroy onBeforeUnmount ❌ 无 改名了!叫"卸载"更准确
destroyed onUnmounted ❌ 无
新增钩子 ❌ 无 onActivated / onDeactivated keep-alive组件用
-------------------------------------------------------------------------------------------------
关键变化:Vue3取消了beforeCreate和created,用setup()函数替代了!
01.vue2生命周期
a.4个阶段 + 8个钩子速查表
阶段 钩子函数 执行时机 常见用途
01.创建阶段 beforeCreate data和methods还未初始化 极少用
created data和methods已初始化,可访问 初始化数据、发起API请求
02.挂载阶段 beforeMount 虚拟DOM已创建,真实DOM未渲染 极少用
mounted 组件已挂载到页面,真实DOM可操作 初始化DOM操作、初始化第三方库
03.运行阶段 beforeUpdate data变化检测到,DOM未重新渲染 获取更新前的DOM状态
updated 数据变化完全渲染到页面 监测DOM更新结果
04.销毁阶段 beforeDestroy 销毁即将开始,实例仍可用 清理定时器、事件监听、全局变量
destroyed 销毁完成,实例不可用 极少用
b.单个组件的执行顺序
a.分类1
组件初始化流程(单次执行)
│
├─ beforeCreate() ← 实例创建,data/methods/el都不存在
├─ created() ✅ ← data/methods已初始化(最早能访问data的地方)
│ • 此时DOM还未渲染
│ • 可以发起API请求、初始化数据
│
├─ beforeMount() ← 模板已编译,DOM即将插入
├─ mounted() ✅ ← 组件已挂载,真实DOM已存在
│ • 可以直接操作DOM(this.$refs、DOM查询等)
│ • 适合初始化第三方库(echarts、jQuery插件等)
│
└─ (进入运行阶段,循环)
b.分类2
组件更新流程(data变化时执行)
│
├─ beforeUpdate() ← 检测到data变化,即将重新渲染
│ • DOM仍是旧值
│ • 可获取更新前的DOM内容
│
├─ updated() ✅ ← 数据变化完全渲染
│ • DOM已是新值
│ • 可获取更新后的DOM内容
│
└─ (监听到下一个data变化,重复上述流程)
c.分类3
组件销毁流程(组件卸载时执行)
│
├─ beforeDestroy() ✅ ← 销毁前,最后一个有效时机
│ • 实例仍完全可用
│ • 清理定时器、事件监听、全局变量、计时器
│
└─ destroyed() ← 销毁完成,实例不再响应
• 实际很少用
c.代码示例
a.基础示例:每个钩子都打印
export default {
data() {
return {
count: 0
}
},
beforeCreate() {
console.log('01-beforeCreate: data/methods都不存在', this.count) // undefined
},
created() {
console.log('02-created: data已初始化', this.count) // 0
// ✅ 最常用:初始化数据、发起API请求
},
beforeMount() {
console.log('03-beforeMount: DOM还未渲染')
},
mounted() {
console.log('04-mounted: DOM已渲染')
// ✅ 最常用:操作DOM、初始化第三方库
this.$refs.myInput?.focus()
},
beforeUpdate() {
console.log('05-beforeUpdate: data变化,DOM未更新')
},
updated() {
console.log('06-updated: DOM已更新')
},
beforeDestroy() {
console.log('07-beforeDestroy: 即将销毁')
// ✅ 必须用:清理资源
},
destroyed() {
console.log('08-destroyed: 销毁完成')
}
}
-------------------------------------------------------------------------------------------------
输出顺序:
01-beforeCreate
02-created
03-beforeMount
04-mounted
← 用户点击按钮修改count
05-beforeUpdate
06-updated
← 组件卸载
07-beforeDestroy
08-destroyed
b.实战示例:在项目中如何用
export default {
data() {
return {
list: [],
timer: null,
myCharts: null
}
},
// ✅ 第一个钩子:初始化数据、发起请求
created() {
this.fetchData()
},
// ✅ 第二个常用钩子:DOM操作、第三方库初始化
mounted() {
// 初始化图表库
this.myCharts = echarts.init(this.$refs.chartContainer)
this.myCharts.setOption(this.chartOption)
// 启动定时器
this.timer = setInterval(() => {
this.count++
}, 1000)
// 添加事件监听
window.addEventListener('resize', this.handleResize)
},
methods: {
async fetchData() {
try {
const res = await fetch('/api/data')
this.list = await res.json()
} catch (e) {
console.error(e)
}
},
handleResize() {
this.myCharts?.resize()
}
},
// ✅ 销毁前必须清理资源
beforeDestroy() {
// 清理定时器
clearInterval(this.timer)
// 移除事件监听
window.removeEventListener('resize', this.handleResize)
// 销毁图表
this.myCharts?.dispose()
// 清空引用
this.myCharts = null
}
}
d.父子组件生命周期执行顺序
a.挂载阶段(初始化)
父 beforeCreate
→ 父 created
→ 父 beforeMount
→ 子 beforeCreate
→ 子 created
→ 子 beforeMount
→ 子 mounted ✅
→ 父 mounted ✅
总结:父组件等待所有子组件挂载完成后,才算挂载完成
b.更新阶段(data变化)
父 beforeUpdate
→ 子 beforeUpdate
→ 子 updated ✅
→ 父 updated ✅
总结:父子组件互不影响,各自独立更新(当有prop传递时才会有这个顺序)
c.销毁阶段(组件卸载)
父 beforeDestroy
→ 子 beforeDestroy
→ 子 destroyed ✅
→ 父 destroyed ✅
总结:子组件先销毁,父组件后销毁(从内到外)
d.核心原则
挂载和更新时,父→子;销毁时,子→父
e.8个钩子的实际作用(纯JS角度)
a.对象化理解
// 模拟Vue内部实现思路
class VueComponent {
constructor() {
// 01. beforeCreate - 框架初始化,业务data还不存在
this.beforeCreate?.call(this)
// 02. created - 业务data初始化完成
this.initData()
this.created?.call(this)
// 03. beforeMount - 模板编译完成,即将插入DOM
this.beforeMount?.call(this)
// 04. mounted - 已插入真实DOM
this.render()
this.mounted?.call(this)
}
update(newData) {
// 05. beforeUpdate - data变化检测到,DOM还未更新
this.beforeUpdate?.call(this)
// 06. updated - 页面DOM已更新
this.patch()
this.updated?.call(this)
}
destroy() {
// 07. beforeDestroy - 销毁前最后一次有效时机
this.beforeDestroy?.call(this)
// 08. destroyed - 销毁完成,无法再用
this.destroyed?.call(this)
}
}
b.JS知识点总结
钩子 对象状态 JS角度理解 何时用
beforeCreate 半初始化 就像new Object()但还未赋值属性 基本不用
created 全初始化 对象属性已完全赋值,可调用方法 初始化、API请求
beforeMount 模板准备 数据准备好了,即将转换为DOM 基本不用
mounted DOM激活 对象已挂载到文档,可操作真实DOM DOM操作、第三方库
beforeUpdate 数据变化 属性已更新,DOM还未刷新 比较少用
updated 完全同步 属性和DOM都已同步 监测DOM结果
beforeDestroy销毁前夕 对象即将销毁,最后的cleanup机会 资源清理
destroyed 已销毁 对象已销毁,无法使用 基本不用
f.最佳实践清单
a.必须做的事
a.在 created 中初始化数据、发起API请求
b.在 mounted 中操作DOM、初始化第三方库(echarts、地图等)
c.在 beforeDestroy 中清理定时器、事件监听、销毁第三方库实例
b.要避免的事
a.不要在 beforeCreate 中访问data(不存在)
b.不要在 mounted 之前操作真实DOM(不存在)
c.不要在 beforeDestroy 之后尝试操作组件(已销毁)
d.不要用箭头函数定义钩子(会导致this指向错误)
c.错误示范
// ❌ 错误1:访问不存在的属性
created: () => {
console.log(this.count) // this指向window
}
// ❌ 错误2:在destroyed之后还要修改
destroyed() {
this.data = 123 // 已销毁,无法修改
}
// ❌ 错误3:定时器未清理
mounted() {
this.timer = setInterval(() => { ... }, 1000)
}
// 组件卸载时定时器仍在运行,造成内存泄漏
d.正确示范
// ✅ 正确1:使用普通函数,this指向组件实例
created() {
console.log(this.count) // 0
}
// ✅ 正确2:在beforeDestroy中清理
beforeDestroy() {
clearInterval(this.timer)
this.timer = null
}
// ✅ 正确3:完整的资源管理
mounted() {
this.timer = setInterval(() => { ... }, 1000)
},
beforeDestroy() {
clearInterval(this.timer)
}
02.vue3生命周期
a.Vue2 vs Vue3 快速对比表
阶段 Vue2 Vue3 Composition API Vue3 Options API 变化说明
创建阶段 beforeCreate 直接写 setup() ❌ 无 完全变了,没有这俩钩子了
created 直接写 setup() ❌ 无
挂载阶段 beforeMount onBeforeMount beforeMount 加on前缀更直观
mounted onMounted ✅ mounted 最常用
更新阶段 beforeUpdate onBeforeUpdate beforeUpdate 逻辑不变,名字改了
updated onUpdated updated 常用
销毁阶段 beforeDestroy onBeforeUnmount ❌ 无 改名了!叫"卸载"更准确
destroyed onUnmounted ❌ 无
新增钩子 ❌ 无 onActivated / onDeactivated keep-alive组件用
关键变化:Vue3取消了beforeCreate和created,用setup()函数替代了!
b.Vue3 生命周期的4个阶段 + 钩子
a.创建阶段 → setup()
// Vue2方式(不推荐,Vue3仍支持)
export default {
beforeCreate() { /* 数据未初始化 */ },
created() { /* data已初始化 */ }
}
-------------------------------------------------------------------------------------------------
// Vue3推荐方式(Composition API + <script setup>)
<script setup>
import { ref, onMounted } from 'vue'
// 直接在setup中初始化,无需beforeCreate/created钩子
const count = ref(0)
const name = ref('John')
// 初始化逻辑直接写这儿
console.log('组件初始化中...') // 等同于Vue2的created
</script>
-------------------------------------------------------------------------------------------------
理解:setup()函数自动执行,相当于Vue2的created阶段。此时data已初始化,但DOM还未创建。
b.挂载阶段 → onBeforeMount / onMounted
// Vue2方式(不推荐)
export default {
beforeMount() { /* DOM即将插入 */ },
mounted() { /* DOM已插入,最常用 */ }
}
-------------------------------------------------------------------------------------------------
// Vue3推荐方式(Composition API)
<script setup>
import { ref, onBeforeMount, onMounted } from 'vue'
const myInput = ref(null)
const chartContainer = ref(null)
// ⚠️ 极少用
onBeforeMount(() => {
console.log('DOM即将挂载,此时真实DOM还不存在')
})
// ✅ 最常用:DOM操作、初始化第三方库
onMounted(() => {
console.log('DOM已挂载到页面')
// 方式1:操作DOM(使用template refs)
myInput.value?.focus()
// 方式2:初始化第三方库
const chart = echarts.init(chartContainer.value)
chart.setOption(options)
// 方式3:发起API请求(有些项目也在这里做)
fetchUserData()
})
</script>
<template>
<input ref="myInput" placeholder="自动获焦" />
<div ref="chartContainer" style="width: 100%; height: 400px"></div>
</template>
-------------------------------------------------------------------------------------------------
理解:onMounted是Vue3中最常用的钩子,相当于Vue2的mounted。
c.更新阶段 → onBeforeUpdate / onUpdated
// Vue2方式(不推荐)
export default {
beforeUpdate() { /* data变化,DOM未更新 */ },
updated() { /* DOM已更新 */ }
}
-------------------------------------------------------------------------------------------------
// Vue3推荐方式(Composition API)
<script setup>
import { ref, onBeforeUpdate, onUpdated, nextTick } from 'vue'
const count = ref(0)
const content = ref('')
// ⚠️ 不常用:获取更新前的状态
onBeforeUpdate(() => {
console.log('数据变化,DOM尚未更新')
console.log('当前DOM内容:', document.getElementById('display')?.textContent)
})
// ⚠️ 很少用:监听DOM更新完成
onUpdated(() => {
console.log('DOM已更新')
// 注意:不要在此修改响应式数据,会导致无限更新循环!
})
// ✅ 推荐用法:使用nextTick代替onUpdated
const increment = async () => {
count.value++
// 等待DOM更新完成后执行
await nextTick()
console.log('DOM已完全更新:', document.getElementById('display')?.textContent)
}
</script>
<template>
<div id="display">{{ count }}</div>
<button @click="increment">+1</button>
</template>
-------------------------------------------------------------------------------------------------
理解:
onBeforeUpdate 和 onUpdated 在组件自身props/data变化时触发
但不推荐在这些钩子里做复杂逻辑,用watch或nextTick代替
在onUpdated里修改响应式数据会造成无限循环!
d.卸载阶段 → onBeforeUnmount / onUnmounted
// Vue2方式(Vue3仍支持Options API写法)
export default {
beforeDestroy() { /* 销毁即将开始 */ },
destroyed() { /* 销毁完成 */ }
}
-------------------------------------------------------------------------------------------------
// Vue3推荐方式(Composition API)
<script setup>
import { ref, onBeforeUnmount, onUnmounted } from 'vue'
let timer = null
let listeners = []
// setup中初始化
onMounted(() => {
// 启动定时器
timer = setInterval(() => {
console.log('定时任务...')
}, 1000)
// 添加事件监听
window.addEventListener('resize', handleResize)
listeners.push(handleResize)
})
// ✅ 销毁前必须清理资源
onBeforeUnmount(() => {
console.log('组件卸载前,清理资源...')
// 清理定时器
clearInterval(timer)
timer = null
// 移除事件监听
window.removeEventListener('resize', handleResize)
// 销毁第三方库
myChart?.dispose()
myChart = null
})
// ⚠️ 极少用
onUnmounted(() => {
console.log('组件已卸载,此时实例已不可用')
})
function handleResize() {
console.log('窗口大小改变')
}
</script>
-------------------------------------------------------------------------------------------------
理解:
Vue3改名为onBeforeUnmount和onUnmounted(原来的beforeDestroy和destroyed)
必须在此清理:定时器、事件监听、库实例,否则造成内存泄漏
onUnmounted基本不用,因为组件已销毁
c.Composition API 钩子的完整写法
a.方式1:<script setup> (推荐)
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const count = ref(0)
onMounted(() => {
console.log('已挂载')
})
onUnmounted(() => {
console.log('已卸载')
})
</script>
<template>
<div>{{ count }}</div>
</template>
b.方式2:setup() 函数
import { ref, onMounted, onUnmounted } from 'vue'
export default {
setup() {
const count = ref(0)
onMounted(() => {
console.log('已挂载')
})
onUnmounted(() => {
console.log('已卸载')
})
return { count }
}
}
c.方式3:Options API(Vue3仍支持,但不推荐)
export default {
data() {
return { count: 0 }
},
mounted() {
console.log('已挂载')
},
unmounted() {
console.log('已卸载')
}
}
d.父子组件生命周期执行顺序(Composition API)
a.挂载过程
父 setup执行
↓
父 onBeforeMount
↓
子 setup执行
↓
子 onBeforeMount
↓
子 onMounted ✅
↓
父 onMounted ✅
→ 最终结果:子组件先挂载,父组件后挂载
b.更新过程
父 onBeforeUpdate
↓
子 onBeforeUpdate
↓
子 onUpdated ✅
↓
父 onUpdated ✅
→ 子组件先更新完成,父组件后更新完成
c.卸载过程
父 onBeforeUnmount
↓
子 onBeforeUnmount
↓
子 onUnmounted ✅
↓
父 onUnmounted ✅
→ 子组件先卸载,父组件后卸载(从内到外)
e.Vue3 新增的钩子
a.onActivated / onDeactivated(for keep-alive)
// 用于<keep-alive>缓存的组件
<script setup>
import { onActivated, onDeactivated } from 'vue'
// 组件被激活(从缓存中取出)
onActivated(() => {
console.log('组件被从缓存中激活')
})
// 组件被停用(被缓存起来)
onDeactivated(() => {
console.log('组件被缓存')
})
</script>
f.实战示例:完整的组件生命周期
a.代码
<script setup>
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
// 数据初始化(等同于setup执行)
const users = ref([])
const loading = ref(false)
const userList = ref(null)
// 方法定义
async function fetchUsers() {
loading.value = true
try {
const res = await fetch('/api/users')
users.value = await res.json()
} catch (e) {
console.error(e)
} finally {
loading.value = false
}
}
function handleScroll() {
console.log('页面滚动')
}
// ✅ 第一个关键钩子:组件已挂载
onMounted(async () => {
console.log('1. 组件已挂载')
// 发起API请求
await fetchUsers()
// 操作DOM
userList.value?.scrollIntoView()
// 添加事件监听
window.addEventListener('scroll', handleScroll)
})
// 监听数据变化
watch(users, (newUsers) => {
console.log('用户列表已更新:', newUsers.length)
})
// ✅ 第二个关键钩子:组件即将卸载
onUnmounted(() => {
console.log('2. 组件卸载前进行清理')
// 移除事件监听
window.removeEventListener('scroll', handleScroll)
})
</script>
<template>
<div ref="userList" class="user-list">
<div v-if="loading" class="loading">加载中...</div>
<div v-else-if="users.length">
<div v-for="user in users" :key="user.id" class="user-item">
{{ user.name }}
</div>
</div>
<div v-else>暂无用户</div>
</div>
</template>
b.输出顺序
1. 组件已挂载
用户列表已更新: 10 (watch触发)
2. 组件卸载前进行清理 (组件被移除时)
g.常见错误汇总
// ❌ 错误1:钩子中使用箭头函数
onMounted(() => {
this.count // this会指向window,不是组件实例
})
// ✅ 正确:Composition API中不需要this
onMounted(() => {
count.value++ // 直接用ref
})
// ❌ 错误2:在onUpdated中修改响应式数据
onUpdated(() => {
count.value++ // 会导致无限更新循环!
})
// ✅ 正确:用watch监听
watch(count, (newVal) => {
console.log('count已更新:', newVal)
})
// ❌ 错误3:定时器未清理
onMounted(() => {
timer = setInterval(() => {}, 1000) // 组件卸载仍在运行 → 内存泄漏
})
// ✅ 正确:在卸载前清理
onBeforeUnmount(() => {
clearInterval(timer)
})
// ❌ 错误4:在异步回调中注册钩子
setTimeout(() => {
onMounted(() => {}) // 太晚了,此时组件实例已丢失
}, 100)
// ✅ 正确:同步注册钩子
onMounted(() => {
setTimeout(() => {
// 在钩子内部异步是没问题的
}, 100)
})
h.最佳实践清单
a.推荐
a.用<script setup>写Composition API(最简洁)
b.在onMounted中发起API请求
c.用watch监听数据变化而不是用updated钩子
d.用nextTick获取更新后的DOM
e.在onBeforeUnmount中清理所有资源
f.创建可复用的Composables函数
g.为关键逻辑添加类型注解
b.避免
a.不要用Options API(除非维护老项目)
b.不要在onUpdated中修改响应式数据
c.不要忘记清理定时器、事件监听
d.不要在onBeforeUnmount之后访问组件数据
e.不要使用全局mixins(用Composables替代)
1.4 data函数
01.回答
组件中的data写成一个函数,数据【以函数返回值形式定义】,这样【每复用一次组件,就会返回一份新的data】
02.说明
类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据。
而单纯的写成对象形式,就使得所有组件实例共用了一份 data,就会造成一个变了全都会变的结果
1.5 scoped原理
01.定义
Vue中的scoped是一种基于属性选择器的样式隔离方案,它并不是传统意义上的CSS模块化方案
它通过【为组件生成唯一的属性选择器】来实现样式的隔离,提高了样式的可维护性和组件的独立性
02.工作原理
a.说明
当在 <style> 标签上使用 scoped 属性时,Vue 会为当前组件的每个元素添加一个唯一的 data-v-xxxxxx 属性
并将样式规则中的选择器修改为包含该属性的形式
b.编译阶段
在编译 .vue 文件时,Vue 的编译器会处理 <style> 标签,具体步骤如下:
解析样式:使用 postcss 解析样式,生成 AST(抽象语法树)
添加属性选择器:遍历 AST,为每个选择器添加 [data-v-xxxxxx] 属性选择器
生成唯一属性:xxxxxx 是一个基于组件文件路径和内容的哈希值,确保唯一性
c.运行时
在运行时,Vue 会为组件的根元素和所有子元素添加 data-v-xxxxxx 属性
03.优点、缺点
a.优点
a.样式隔离
scoped可以有效地防止组件间的样式冲突,确保每个组件的样式都是独立的
b.提高可维护性
由于样式被限制在组件内部,因此当需要修改或调试样式时,可以更容易地定位到相关的组件和样式
b.缺点
a.性能影响
虽然scoped样式带来了很多好处,但由于需要为每个组件生成唯一的属性选择器和修改样式选择器
因此可能会对性能产生一定的影响,然而,在大多数情况下,这种影响是可以接受的
b.无法使用全局样式库
如果需要使用全局样式库(如Bootstrap),则需要在全局样式文件中引入,而不能在scoped样式中使用
这是因为scoped样式被限制在组件内部,无法应用到全局元素上
c.深度选择器
在某些情况下,你可能需要为scoped样式添加一个全局样式或修改子组件的样式
这时可以使用深度选择器(如Vue 3中的::v-deep或Vue 2中的/deep/、>>>)来“穿透”scoped限制
但需要注意的是,过度使用深度选择器可能会破坏样式的隔离性
04.示例
a.假设有以下组件
<template>
<div class="example">Hello World</div>
</template>
<style scoped>
.example {
color: red;
}
</style>
b.编译后生成的样式和模板如下
<div class="example" data-v-f3f3eg9>Hello World</div>
<style>
.example[data-v-f3f3eg9] {
color: red;
}
</style>
1.6 computed、watch
01.场景
computed:一般用在模板渲染中,某个值是【依赖了其它的响应式对象】,甚至是计算属性计算而来 几组值
watch:适用于观测【某个值的变化】去完成一段复杂的业务逻辑 某个值
02.区别
computed:计算属性,依赖其他属性计算值,computed的值有缓存,只有当计算值变化才会返回内容,可以设置getter和setter
watch:监听到值的变化就会执行回调,在回调中可以进行一些逻辑操作
1.7 组合式API
01.Setup内部API
a.响应式 API
API | 描述 | 示例
--------------|------------------------|------------------------------------------------
ref() | 创建响应式引用 | const count = ref(0)
reactive() | 创建响应式对象 | const state = reactive({})
computed() | 计算属性 | const double = computed(() => count.value * 2)
readonly() | 只读代理 | const readonlyState = readonly(state)
watchEffect() | 自动追踪依赖的副作用 | watchEffect(() => {})
watch() | 侦听器 | watch(source, callback)
b.生命周期钩子
钩子 | 对应 Options API | 触发时机
-----------------|------------------|----------------
onBeforeMount() | beforeMount | 挂载前
onMounted() | mounted | 挂载后
onBeforeUpdate() | beforeUpdate | 更新前
onUpdated() | updated | 更新后
onBeforeUnmount() | beforeUnmount | 卸载前
onUnmounted() | unmounted | 卸载后
onErrorCaptured() | errorCaptured | 错误捕获
onActivated() | activated | keep-alive 激活
onDeactivated() | deactivated | keep-alive 失活
c.依赖注入
// 提供数据
provide('theme', 'dark')
provide('userInfo', reactive({ name: 'John' }))
// 注入数据
const theme = inject('theme')
const userInfo = inject('userInfo', defaultValue) // 带默认值
d.模板引用
// 元素引用
const inputRef = ref()
// 组件引用
const childRef = ref()
// 在模板中使用
// <input ref="inputRef" />
// <ChildComponent ref="childRef" />
02.外部函数调用方式
a.方法一:直接在 Setup 中调用
// 外部函数
const removePhoto = (formComposable) => {
formComposable.dataForm.photoUrl = ""
}
export default {
setup() {
const formComposable = useForm()
// 包装器方法
const handleRemovePhoto = () => {
removePhoto(formComposable)
}
return {
handleRemovePhoto
}
}
}
b.方法二:通过组合式函数
// composables/usePhoto.js
export function usePhoto(formComposable) {
const removePhoto = () => {
formComposable.dataForm.photoUrl = ""
}
return {
removePhoto
}
}
// 组件中使用
export default {
setup() {
const formComposable = useForm()
const { removePhoto } = usePhoto(formComposable)
return {
removePhoto
}
}
}
c.方法三:模块化导入
// utils/photoUtils.js
export const removePhoto = (formComposable) => {
formComposable.dataForm.photoUrl = ""
}
export function deleteEducation(index, subTableComposable) {
subTableComposable.educationList.value.splice(index, 1)
}
export async function saveEducation(subTableComposable) {
await api.saveEducation(subTableComposable.form)
}
// 组件中导入使用
import { removePhoto, deleteEducation, saveEducation } from './utils/photoUtils'
export default {
setup() {
const formComposable = useForm()
const subTableComposable = useSubTable()
const handleRemovePhoto = () => removePhoto(formComposable)
const handleDeleteEducation = (index) => deleteEducation(index, subTableComposable)
const handleSaveEducation = () => saveEducation(subTableComposable)
return {
handleRemovePhoto,
handleDeleteEducation,
handleSaveEducation
}
}
}
03.最佳实践
a.代码组织结构
export default {
name: 'ComponentName',
components: {},
props: {},
emits: [],
setup(props, { emit }) {
// 1. 响应式数据
const loading = ref(false)
const form = reactive({})
// 2. 计算属性
const computedValue = computed(() => {})
// 3. 方法定义
const handleSubmit = async () => {}
// 4. 生命周期
onMounted(() => {})
// 5. 监听器
watch(loading, () => {})
// 6. 返回暴露
return {
loading,
form,
computedValue,
handleSubmit
}
}
}
b.外部函数组织
// utils/businessLogic.js - 纯业务逻辑
export const validateForm = (data) => { /* ... */ }
export const formatData = (data) => { /* ... */ }
// composables/useFeature.js - 组合式逻辑
export function useFeature() {
const state = ref()
const method = () => {}
return { state, method }
}
// 在组件中统一导入使用
import { validateForm, formatData } from './utils/businessLogic'
import { useFeature } from './composables/useFeature'
c.类型安全(TypeScript)
interface FormData {
name: string
email: string
}
// 外部函数类型定义
const removePhoto = (formComposable: FormComposable): void => {
formComposable.dataForm.photoUrl = ""
}
// Setup 中的类型推导
export default defineComponent({
setup(): {
handleRemovePhoto: () => void
} {
const formComposable = useForm()
const handleRemovePhoto = (): void => {
removePhoto(formComposable)
}
return {
handleRemovePhoto
}
}
})
04.常见问题
a.为什么不能在 setup 外部使用 onMounted?
生命周期钩子依赖于当前组件实例,只能在 setup 或组合式函数内部调用。
b.外部函数如何访问响应式数据?
通过参数传递或返回包装函数的方式
-----------------------------------------------------------------------------------------------------
// 方式1: 参数传递
const externalFn = (reactiveData) => {
reactiveData.value = 'new value'
}
// 方式2: 返回包装函数
const createHandler = (reactiveData) => {
return () => {
reactiveData.value = 'new value'
}
}
c.如何在模板中直接使用外部函数?
必须在 setup 的 return 中暴露:
-----------------------------------------------------------------------------------------------------
export default {
setup() {
return {
// 直接暴露外部函数
externalFunction,
// 或包装后暴露
handleClick: () => externalFunction(params)
}
}
}
d.组合式函数与外部函数的区别?
组合式函数:可以使用 Vue 的响应式 API,通常以 use 开头
外部函数:纯 JavaScript 函数,不依赖 Vue 响应式系统
05.完整代码示例
// 外部工具函数 - setup 外部
const removePhoto = (formComposable) => {
formComposable.dataForm.photoUrl = ""
}
function deleteEducation(index, subTableComposable) {
subTableComposable.educationList.value.splice(index, 1)
}
async function saveEducation(subTableComposable) {
// 异步保存逻辑
await axios.post('/api/education', subTableComposable.educationForm)
}
export default {
//************************** Setup 平级标签 **************************
/** 组件名称 */
name: "ComponentName",
/** 继承的组件 */
extends: ParentComponent,
/** 混入 */
mixins: [mixin1, mixin2],
/** 子组件注册 */
components: {
ArrowDown,
CustomButton
},
/** 指令注册 */
directives: {
focus: focusDirective
},
/** Props 声明 */
props: {
title: String,
modelValue: [String, Number]
},
/** 事件声明 */
emits: ['update:modelValue', 'submit'],
/** 插槽声明 */
slots: ['default', 'header'],
/** 暴露给模板引用 */
expose: ['methodName', 'propertyName'],
/** 继承属性控制 */
inheritAttrs: false,
//************************** Setup 函数 **************************
setup(props, { emit, slots, attrs, expose }) {
//************************** Setup 内部标签 **************************
// 响应式数据
const loading = ref(false)
const formData = reactive({})
// 计算属性
const computedValue = computed(() => formData.value * 2)
// 监听器
watch(loading, (newVal) => {
console.log('Loading changed:', newVal)
})
watchEffect(() => {
console.log('Effect triggered')
})
// 生命周期钩子
onBeforeMount(() => {})
onMounted(() => {})
onBeforeUpdate(() => {})
onUpdated(() => {})
onBeforeUnmount(() => {})
onUnmounted(() => {})
onErrorCaptured(() => {})
onRenderTracked(() => {})
onRenderTriggered(() => {})
onActivated(() => {}) // keep-alive
onDeactivated(() => {}) // keep-alive
onServerPrefetch(() => {}) // SSR
// 依赖注入
provide('key', 'value')
const injectedValue = inject('parentKey')
// 模板引用
const elementRef = ref()
const componentRef = ref()
// 异步组件
const AsyncComp = defineAsyncComponent(() =>
import('./AsyncComponent.vue')
)
// 外部函数调用方法
const handleRemovePhoto = () => {
removePhoto(formComposable) // 直接调用外部函数
}
const handleDeleteEducation = (index) => {
deleteEducation(index, subTableComposable) // 调用外部函数
}
const handleSaveEducation = async () => {
await saveEducation(subTableComposable) // 调用外部异步函数
}
// 暴露给模板的数据和方法
return {
loading,
formData,
computedValue,
elementRef,
componentRef,
handleRemovePhoto,
handleDeleteEducation,
handleSaveEducation,
// 直接暴露外部函数(可选)
removePhoto: () => removePhoto(formComposable),
deleteEducation: (index) => deleteEducation(index, subTableComposable),
saveEducation: () => saveEducation(subTableComposable)
}
}
}
1.8 vuex、pinia
00.汇总
a.分类1
a.JS类比
概念 定义 JS类比 何时用
State 应用的集中数据源 全局变量/单例对象 存放全局数据
Getters 派生状态(计算属性) Object.defineProperty或get方法 读取或处理状态
Mutations 修改状态的函数(同步) 数组方法(push、pop等) 直接改变状态
Actions 业务逻辑的处理(异步) async/await的业务方法 异步操作、API调用
Modules 状态的模块化分割 类、命名空间、对象嵌套 大型应用分割状态
b.Vuex vs Pinia
对比项 Vuex Pinia
设计年代 Vue2时代 Vue3时代
核心架构 分层严格(State→Mutations→Actions→Getters) 简化合并(直接修改+方法调用)
Mutations 必须有 已删除,action同时处理
同异步处理 分开(mutations同步,actions异步) 合并(action统一处理)
代码行数 较多(配置繁琐) 较少(简洁直观)
TypeScript支持 较差 优秀
学习曲线 较陡(需要理解5个概念) 较平缓(就像写普通JS)
模块化 Modules嵌套 天然多Store分离
性能 (更轻量)
适用版本 Vue2 & 3 Vue3(也支持Vue2)
官方推荐 维持更新 新项目首选
b.分类2
a.vuex/pinia共同点
Vuex 和 Pinia 一样
天生就是用来做“跨组件”和 SPA 内的“跨页面”状态共享的。这是它们共同的核心价值。
全局状态管理系统,用于多个组件中数据共享、数据缓存等
-------------------------------------------------------------------------------------------------
Store:定义
State:类似data
Getter:类似computed
Action:类似methods
Plugin:插件
-------------------------------------------------------------------------------------------------
使用:选项式
使用:组合式
b.vuex四层架构
组件 (dispatch/commit)
↓
Actions (异步) ← 所有异步操作都在这里
↓ commit
Mutations (同步) ← 唯一修改state的地方
↓
State (真实数据)
↓
Getters (派生数据) ← 计算属性
↓
组件 (读取显示)
c.pinia
组件
↓
Store (同时包含state、actions、getters)
↓ 直接修改 / 方法调用
↓
更新完成
c.分类3
a.概念
State:定义了应用状态的数据结构,可以在这里设置默认的初始状态
Getter:允许组件从 Store 中获取数据,mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性
Mutation:是唯一更改 store 中状态的方法,且必须是同步函数
Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作
Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中
b.Vuex页面刷新数据丢失怎么解决
a.本地存储的方案来保存数据
自己设计存储方案 / 第三方插件
b.使用vuex-persist插件
为Vuex持久化存储而生的一个插件
不需要你手动存取storage,而是直接将状态保存至cookie或者localStorage中
c.Vuex为什么要分模块并且加命名空间
a.模块
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象
当应用变得非常复杂时,store 对象就有可能变得相当臃肿
为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)
每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块
b.命名空间
默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间
这样使得多个模块能够对同一 mutation 或 action 作出响应
如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块
当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名
01.Vuex(Vue2时代的标准方案)
a.Vuex的4层架构
组件 (dispatch/commit)
↓
Actions (异步) ← 所有异步操作都在这里
↓ commit
Mutations (同步) ← 唯一修改state的地方
↓
State (真实数据)
↓
Getters (派生数据) ← 计算属性
↓
组件 (读取显示)
b.完整代码示例
a.定义Vuex Store
// store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
// 01. State - 全局数据源
state: {
count: 0,
users: [],
token: ''
},
// 02. Getters - 计算状态(类似computed)
getters: {
// 直接读取
getCount: state => state.count,
// 计算处理
doubleCount: state => state.count * 2,
// 根据参数返回
getUserById: state => userId => {
return state.users.find(u => u.id === userId)
}
},
// 03. Mutations - 同步修改状态
// ⚠️ 必须是同步的!
mutations: {
// 最简单的形式
INCREMENT(state) {
state.count++
},
// 接收参数(payload)
SET_COUNT(state, newCount) {
state.count = newCount
},
// 处理数组
ADD_USER(state, user) {
state.users.push(user)
},
SET_TOKEN(state, token) {
state.token = token
}
},
// 04. Actions - 异步操作和业务逻辑
// ✅ 异步操作都放这里
actions: {
// 最简单的形式(提交mutation)
increment(context) {
context.commit('INCREMENT')
},
// 异步操作:API请求
async fetchUsers({ commit }) {
try {
const response = await fetch('/api/users')
const users = await response.json()
commit('SET_USERS', users)
} catch (e) {
console.error(e)
}
},
// 异步操作:登录
async login({ commit }, credentials) {
try {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials)
})
const data = await response.json()
commit('SET_TOKEN', data.token)
return { success: true }
} catch (e) {
return { success: false, error: e.message }
}
},
// 调用其他action
async initApp({ dispatch }) {
await dispatch('fetchUsers')
await dispatch('checkAuth')
}
}
})
b.在组件中使用
<template>
<div>
<!-- 直接访问state -->
<p>Count: {{ $store.state.count }}</p>
<!-- 使用getter -->
<p>Double: {{ $store.getters.doubleCount }}</p>
<!-- 触发mutation(同步) -->
<button @click="$store.commit('INCREMENT')">+1</button>
<!-- 触发action(异步) -->
<button @click="$store.dispatch('login', {username: 'john'})">登录</button>
<!-- 使用辅助函数 -->
<p>Users: {{ userList.length }}</p>
</div>
</template>
<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'
export default {
computed: {
// 方式1:直接访问
count() {
return this.$store.state.count
},
// 方式2:使用mapState
...mapState(['count', 'users', 'token']),
// 方式3:使用mapGetters
...mapGetters(['doubleCount'])
},
methods: {
// 方式1:直接commit/dispatch
increment() {
this.$store.commit('INCREMENT')
},
// 方式2:使用mapMutations
...mapMutations(['INCREMENT']),
// 方式3:使用mapActions
...mapActions(['login', 'fetchUsers'])
}
}
</script>
c.Vuex的核心规则
// ✅ 正确做法
// 1. 只能通过mutation修改state
store.commit('SET_COUNT', 10)
// 2. 异步操作必须在action中
store.dispatch('fetchData') // 在action内部commit mutation
// ❌ 错误做法
// 1. 直接修改state(响应式会丢失)
store.state.count = 10 // 不行!
// 2. 在mutation中异步操作
mutations: {
async setData(state, data) { // ❌ 不要这样!
const res = await fetch('/api/data')
}
}
// 3. 在action中直接修改state
actions: {
increment({ state }) {
state.count++ // ❌ 不行!应该commit mutation
}
}
02.Pinia(Vue3推荐方案)
a.Pinia的简化架构
组件
↓
Store (同时包含state、actions、getters)
↓ 直接修改 / 方法调用
↓
更新完成
---
关键区别:Pinia 合并了 mutations 和 actions,无需区分同步异步!
b.完整代码示例
a.定义Pinia Store
// stores/userStore.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
// 01. State - 用ref定义响应式数据
const count = ref(0)
const users = ref([])
const token = ref('')
// 02. Getters - 用computed定义派生状态
const doubleCount = computed(() => count.value * 2)
const getUserById = computed(() => (userId) => {
return users.value.find(u => u.id === userId)
})
// 03. Actions - 同时处理同步和异步(无需区分mutation)
// 同步操作
function increment() {
count.value++
}
function setCount(newCount) {
count.value = newCount
}
function addUser(user) {
users.value.push(user)
}
// 异步操作 - 直接在action中处理
async function fetchUsers() {
try {
const response = await fetch('/api/users')
users.value = await response.json()
} catch (e) {
console.error(e)
}
}
async function login(credentials) {
try {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials)
})
const data = await response.json()
token.value = data.token
return { success: true }
} catch (e) {
return { success: false, error: e.message }
}
}
// 暴露状态和方法
return {
// state
count,
users,
token,
// getters
doubleCount,
getUserById,
// actions
increment,
setCount,
addUser,
fetchUsers,
login
}
})
b.在组件中使用
<template>
<div>
<!-- 直接访问 -->
<p>Count: {{ userStore.count }}</p>
<p>Double: {{ userStore.doubleCount }}</p>
<!-- 直接调用方法 -->
<button @click="userStore.increment()">+1</button>
<button @click="userStore.login({username: 'john'})">登录</button>
</div>
</template>
<script setup>
import { useUserStore } from '@/stores/userStore'
// ✅ 超简单:一句话引入,直接用
const userStore = useUserStore()
// ✅ 可以解构(但推荐不要解构,否则失去响应性)
// const { count, increment } = userStore // ⚠️ 这样会失去响应性!
// ✅ 正确解构方式(使用storeToRefs)
import { storeToRefs } from 'pinia'
const { count, users } = storeToRefs(userStore)
</script>
c.Pinia的核心规则
// ✅ 正确做法
const store = useMyStore()
// 1. 直接修改state(自动响应式)
store.count = 10
// 2. 调用action(同时支持同步和异步)
store.increment()
await store.fetchData()
// ✅ 优点:逻辑简单,无需记住各种规则
// ❌ 不推荐
// 不要这样使用(会丢失响应性)
const { count } = store // ❌ 单纯解构会失去响应性
1.9 mixins、plugin
01.一句话总结
a.Mixins
Mixins 是将可复用的组件选项(data、methods、生命周期等)混合到目标组件中,通过选项合并扩展组件功能,但作用范围仅限单个或多个组件层级。
b.Plugin
Plugin 是通过 app.use() 方法安装的功能包,在 install 方法中定义全局功能(属性、方法、指令、组件等),避免代码重复,提高代码复用性。
c.两者对比
Mixin 是组件级别的选项合并(作用于单个或指定组件)
Plugin 是应用级别的全局扩展(作用于整个应用的所有组件)
02.更多角度
a.对比1
对比维度 | Mixins | Plugin
---------|--------------------|--------------------
作用范围 | 组件级(局部) | 应用级(全局)
安装方式 | mixins: [mixin] | app.use(plugin)
工作原理 | 选项合并 | 执行install方法
何时用 | 组件代码复用 | 全局功能扩展
副作用 | 命名冲突、来源不透明 | 全局污染(需谨慎)
Vue3推荐 | 用Composables代替 | 官方推荐
b.对比2
对比项 | Plugin | Mixin | Component
----------|-------------------|----------------|----------------
作用范围 | 应用级 | 组件级 | 组件级
注册方式 | app.use() | mixins: [] | app.component()
生效对象 | 所有组件 | 指定组件 | 指定位置
能做什么 | 全局配置、指令、组件 | 组件选项合并 | UI复用
推荐度 | | (不推荐) |
何时用 | 全局功能 | 组件逻辑复用 | UI复用
c.选择建议
问题:我想复用一些代码
├─ 如果是"组件级"逻辑复用
│ └─ Vue3: 用 Composables
│ └─ Vue2: 用 Mixin (次选)
├─ 如果是"应用级"全局功能
│ └─ 用 Plugin
│ └─ 如果是第三方库(Vue Router、Pinia等)
└─ 如果同时需要两者
└─ 组件逻辑用Composables,全局功能用Plugin
d.最终结论
1.Vue2 时代:Mixin 是主流,Plugin 用于库集成
2.Vue3 时代:Composables 替代 Mixin,Plugin 是官方推荐的应用扩展方案
3.实际项目:优先级 → Plugin(全局) > Composables(组件级) > Mixin(不推荐)
2 router
2.1 路由方式:汇总
01.常用信息
Vue Router通过beforeEach全局守卫拦截导航,结合addRoute()动态添加路由来实现权限控制;
Hash模式简单但丑(#号),History模式美观但需服务器配置。
02.常用信息
a.核心概念速查表
概念 | 定义 | JS类比 | 何时用
-----------|---------------------------|---------------|------------------------
Router | 路由管理器,管理所有路由规则 | 对象/映射表 | 应用初始化时创建
Route | 单个路由规则 | 对象属性 | 路由配置中定义
beforeEach | 全局前置守卫 | 拦截器/中间件 | 权限检查、登录态验证
addRoute | 动态添加路由 | 对象添加属性 | 权限控制、动态菜单
Hash | URL的#模式 | 浏览器特性 | 简单项目、旧浏览器
History | 浏览器History API | HTML5标准 | 现代项目、美观URL
b.核心JS知识点总结表
JS特性 | 在路由中的用法 | 目的
---------------|---------------------------------------|--------------------------
闭包 | beforeEach/beforeResolve守卫中缓存状态 | 保存权限信息、用户状态
递归 | 动态路由中递归遍历子路由 | 支持嵌套路由的权限控制
异步/Promise | beforeEach/beforeResolve异步执行 | 等待权限API返回再决策
数组过滤 | filter()筛选有权限的路由 | 根据权限过滤动态路由
Map/Object遍历 | 遍历路由配置和权限标识 | 对应权限标识和路由规则
事件系统 | hashchange/popstate事件监听 | 检测URL变化
History API | pushState/replaceState | 管理浏览器历史记录
链式调用 | router.push().then() | 路由导航链
2.2 路由方式1:vue-router
01.基本信息
a.钩子函数种类
全局守卫、路由守卫、组件守卫
b.完整的导航解析流程
导航被触发
在失活的组件里调用 beforeRouteLeave 守卫
调用全局的 beforeEach 守卫
在重用的组件里调用 beforeRouteUpdate 守卫
在路由配置里调用 beforeEnter
解析异步路由组件
在被激活的组件里调用 beforeRouteEnter
调用全局的 beforeResolve 守卫
导航被确认
调用全局的 afterEach 钩子
触发 DOM 更新
调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入
c.流程
旧组件离开 → 全局检查 → 新路由检查 → 新组件准备 → 全局后置 → DOM更新 → 组件创建
beforeLeave → beforeEach → beforeEnter → beforeResolve → afterEach → mount → callback
02.路由时机
a.何时用哪个守卫
场景 | 使用守卫 | 代码位置
---------------|---------------------------|--------------------------------------
检查登录状态 | beforeEach | router/permission.js
检查权限 | beforeEach 或 beforeEnter | router/permission.js 或 路由配置
添加动态路由 | beforeEach | router/permission.js
关闭loading | afterEach | router/permission.js
组件离开前确认 | beforeRouteLeave | 组件内
组件内更新数据 | beforeRouteUpdate | 组件内
组件进入后处理 | beforeRouteEnter 的callback | 组件内
b.总结一句话
Vue Router通过beforeEach全局守卫拦截导航,结合addRoute()动态添加路由来实现权限控制;
Hash模式简单但丑(#号),History模式美观但需服务器配置。
03.路由守卫的3个级别
a.级别1:全局守卫
---
// beforeEach - 最常用:权限检查、登录验证
router.beforeEach(async (to, from) => {
// to: 要去的路由
// from: 来自的路由
const token = localStorage.getItem('token')
// 方式1:返回false中断导航
if (!token && !to.path.includes('/login')) {
return false // 阻止导航
}
// 方式2:返回路径进行重定向
if (!token && !to.path.includes('/login')) {
return '/login' // 重定向到登录
}
// 方式3:返回对象进行高级重定向
if (!token && to.name !== 'Login') {
return { name: 'Login' } // 命名路由重定向
}
// 方式4:直接返回undefined或true,允许导航
return true
})
// beforeResolve - 比beforeEach更晚执行,所有其他守卫都通过后才执行
router.beforeResolve(async (to, from) => {
// 适合需要所有子组件守卫都通过后才执行的逻辑
if (to.meta.requiresCamera) {
try {
await requestCamera()
} catch (e) {
return false
}
}
})
// afterEach - 后置守卫,无法阻止导航
router.afterEach((to, from, failure) => {
// to和from已经切换完成
// 适合关闭loading、保存分析数据等
console.log('路由已完成:', to.path)
})
---
b.级别2:路由守卫(单个路由配置中)
---
const routes = [
{
path: '/admin',
component: Admin,
// beforeEnter只在进入这个路由时触发
beforeEnter: (to, from) => {
// 检查是否是管理员
const user = JSON.parse(localStorage.getItem('user'))
if (user?.role !== 'admin') {
return '/home' // 非管理员重定向
}
}
}
]
---
c.级别3:组件守卫(组件内部)
---
export default {
name: 'MyComponent',
// beforeRouteEnter - 进入该组件路由前调用
// 此时组件实例还未创建!
beforeRouteEnter(to, from, next) {
next((vm) => {
// next的回调函数会在组件挂载后执行
// 此时vm就是组件实例
vm.someMethod()
})
},
// beforeRouteUpdate - 路由改变但组件被重用时调用
// 场景:/users/1 → /users/2,都用UserDetail组件
beforeRouteUpdate(to, from) {
// 可以 this 访问组件实例
this.loadUserData(to.params.id)
},
// beforeRouteLeave - 离开该组件路由前调用
beforeRouteLeave(to, from) {
const answer = window.confirm('确定要离开吗?')
if (!answer) return false // 阻止离开
}
}
2.3 路由方式2:hash、history
00.总结
a.项目需求
如果需要支持旧版浏览器或不想配置服务器,选择【Hash】模式
如果需要美观的 URL 和更好的 SEO 支持,选择【History】模式
b.服务器配置
使用 History 模式时,确保服务器正确配置以处理所有路由请求
c.工作原理对比
对比项 | Hash模式 | History模式
------------|------------------------------|------------------------
URL示例 | http://example.com/#/about | http://example.com/about
实现机制 | 监听hashchange事件 | 使用HTML5 History API
发送到服务器 | 不发送(#后面的内容) | 完整发送
SEO支持 | 不友好 | 友好
浏览器兼容 | 所有浏览器 | 需要HTML5支持
服务器配置 | 不需要 | 需要配置
美观度 | 有#号 | 美观
01.Hash模式
a.定义
Hash 模式使用 URL 的 hash(#)部分来模拟完整的 URL,从而在不重新加载页面的情况下实现路由切换
b.工作原理
URL 中的 hash 部分(#后面的内容)不会被发送到服务器
浏览器的 hashchange 事件用于监听 URL 的变化
c.优点
简单易用,兼容性好,支持所有浏览器,包括不支持 HTML5 History API 的旧浏览器
不需要服务器配置,因为 hash 部分不会被发送到服务器
d.缺点
URL 中包含 #,不够美观
对 SEO 不友好,因为搜索引擎通常不会索引 # 后面的内容
e.示例
const router = new VueRouter({
mode: 'hash',
routes: [
{ path: '/', component: Home },
{ path: '/about', component: About }
]
});
02.History模式
a.定义
History 模式依赖于 HTML5 History API 来实现 URL 路径的变化,而不需要使用 hash
b.工作原理
使用 pushState 和 replaceState 方法来操作浏览器的历史记录
允许使用正常的 URL 结构(不带 #)
c.优点
URL 美观,没有 #
更好的 SEO 支持,因为 URL 是完整的路径
d.缺点
需要服务器配置支持,将所有路由请求重定向到入口 HTML 文件
不支持 HTML5 History API 的浏览器无法使用
e.服务器配置示例(以 Nginx 为例)
location / {
try_files $uri $uri/ /index.html;
}
f.示例
const router = new VueRouter({
mode: 'history',
routes: [
{ path: '/', component: Home },
{ path: '/about', component: About }
]
});
03.工作原理
a.Hash模式原理
// Hash模式:监听URL的#部分变化
window.addEventListener('hashchange', () => {
const hash = location.hash // 获取#后的内容
// 根据hash渲染对应组件
render(hash)
})
// 举例:
// http://example.com/#/user/1
// location.hash = '#/user/1'
// 服务器不会收到/user/1,只会收到/
b.History模式原理
// History模式:使用pushState/replaceState操作历史记录
window.history.pushState(state, title, url)
// state: 保存在history中的对象(可在popstate事件中获取)
// title: 页面标题(基本没用)
// url: 新URL
// 举例:
window.history.pushState(
{ page: 1 }, // 状态对象
null,
'/user/1' // 新URL完整发送给服务器
)
// 监听前进后退:
window.addEventListener('popstate', (event) => {
const state = event.state // 获取之前保存的状态
render(state)
})
// 关键区别:
// pushState/replaceState 改变URL不会触发popstate
// 只有前进/后退才会触发popstate
c.History模式的服务器配置
# Nginx配置示例
location / {
try_files $uri $uri/ /index.html;
}
# 含义:
# 1. 尝试请求的文件 ($uri)
# 2. 尝试请求的目录 ($uri/)
# 3. 都不存在就返回 /index.html (SPA入口)
---
---
// Express/Node服务器配置
app.use(express.static('public'))
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'))
})
2.4 实战场景:动态路由
00.实现思路
a.动态路由
将拥有权限的路由单独从路由表系统中提出,视为动态路由,待后续根据用户权限来导入拥有权限的动态路由
b.权限与动态路由绑定
用户登录之后,需要获取该用户所拥有的权限,该权限前后端需要协调一致,需要对应前端拥有权限的动态路由
后端可以返回动态路由的name来做以区分,也可以在前端单独定义每个动态路由(权限)的标识进行权限的区分
c.获取权限筛选动态路由
获取到用户所拥有的权限之后,将后端返回的权限标识与动态路由中所定义的权限标识比对
筛选出该用户所拥有的权限(动态路由)
d.添加权限(动态路由)
使用router.addRoute()方法将拥有权限的动态路由添加到路由表系统中实现权限的控制
e.渲染菜单
获取到拥有权限的动态路由之后,在vuex/pinia中去组合需要渲染菜单的动态路由(+/静态路由)
菜单需要在路由中按需定义菜单的名称、图片、icon等等,最后将路由中组合好的菜单循环渲染展示
f.退出登录删除动态路由(权限)
用户退出登录时需要删除动态路由(权限),因为如果退出登录时不删除动态路由(权限)
紧接着登录其他权限不同的用户,该用户没有上一个用户所拥有的权限,所以无法查看上一个用户所看到的菜单以及页面
但是该用户可以使用浏览器的回退功能或者修改url地址栏的路径去访问上一个用户能访问的页面
这样该用户就可以访问到不属于自己权限的页面,所以在用户退出登录时需要删除动态路由(权限)
来保证权限的准确性。删除动态路由使用到router.removeRoute()
01.实战场景1
a.提取动态路由
a.说明
每个动态路由中的meta对象中定义了要渲染菜单的名称的title和图片img
permissionCode定义的标识为与后端确定的该用户是否具有该路由菜单的权限
b.代码
export const changeChildRouter = [
{
path: 'user',
name: 'SysUser',
component: () => import(/* webpackChunkName:"system" */'../../views/System/UserManage'),
meta: {
title: '用户管理',
permissionCode: 10004,
img: require('@/assets/images/Tree/setting-user.png')
}
},
{
path: 'department',
name: 'SysDepartment',
component: () => import(/* webpackChunkName:"system" */'../../views/System/DepartmentManage'),
meta: {
title: '部门管理',
permissionCode: 10404,
img: require('@/assets/images/Tree/setting-gis.png')
}
},
{
path: 'position',
name: 'SysPosition',
component: () => import(/* webpackChunkName:"system" */'../../views/System/PositionManage'),
meta: {
title: '岗位管理',
permissionCode: 10204,
img: require('@/assets/images/Tree/setting-user.png')
}
},
{
path: 'role',
name: 'SysRole',
component: () => import(/* webpackChunkName:"system" */'../../views/System/RoleManage'),
meta: {
title: '角色管理',
permissionCode: 10104,
img: require('@/assets/images/Tree/setting-user.png')
}
},
]
b.登录之后,获取该登录用户的权限,筛选动态路由
a.说明
a.第一种想法
当用户登录之后,紧接着在逻辑当中获取该用户的权限,将用户的权限存储到session当中
目的是为了防止用户刷新浏览器权限依然不会丢失
但是安全性不高,用户手动修改session中存储的权限就会导致权限丢失
或者非法获取其他用户的session权限放置未拥有该权限的用户的session中
导致该用户拥有了其他用户不属于自己的权限等等问题
b.第二种想法
当用户登录之后,在路由前置守卫router.beforeEach()中去获取权限
获取到权限之后,引入并筛选出该用户权限的动态路由
使用router.addRoute()将该用户的动态路由添加到路由表系统当中
将筛选出的动态路由(权限)存储到vuex当中
由于vuex做临时存储,所以原理上刷新页面之后vuex中的数据就会丢失
但是在router.beforeEach()路由前置守卫中获取权限就解决了该问题
每次刷新页面都会先执行router.beforeEach()
所以每次刷新页面都会先去获取该用户的权限
根据后端返回的权限标识筛选出的动态路由存储到vuex当中
但是想要实现刷新页面动态路由(权限页面)不丢失
不能将404页面定义为静态路由放置在路由表系统当中
需要随权限获取后,动态添加404页面
目的是为了解决刷新权限页面之后权限丢失,在没有重新获取到权限的时候
检测到路由表系统当中不存在该动态路由,所以直接进入404页面的问题
b.代码
import router from "@/router"
import store from '@/store'
import { changeChildRouter } from '@/router/system'
let routeFlag = false;
router.beforeEach(async (to, from, next) => {
let token = localStorage.getItem('token');
if (!token && !to.path.includes('/login')) {
routeFlag = false
next('/login')
} else if (to.path.includes('/login')) {
routeFlag = false
next()
} else {
if (routeFlag) return next()
routeFlag = true
const res = await store.dispatch('menu/MenuInfo')
const Liststr = res.map(item => item.roleId)
const systemRoutes = systemAsyncRouter(changeChildRouter, Liststr)
asyncRoutes(systemRoutes)
router.addRoute(
{
path: '/:pathMatch(.*)*',
name: '404',
component: () => import(/* common */'@/views/NotFound'),
meta: { title: '404' }
},
)
next({
...to,
replace: true
})
}
})
function systemAsyncRouter(routes, codes) {
const filterRoutes = routes.filter(item => codes.includes(item.meta?.permissionCode))
addAsyncRouter('系统功能', filterRoutes)
store.commit('menu/setMenuList', filterRoutes)
return filterRoutes
}
function addAsyncRouter(parentName, asyncRoutesList) {
asyncRoutesList.map(item => {
if (!item.component) return addAsyncRouter(parentName, item.children)
router.addRoute(parentName, item)
})
}
function asyncRoutes(...args) {
store.commit('menu/setPermissionRouteList', [].concat(...args))
}
c.添加动态路由之后
a.说明
需要在用户退出的时候调用以下方法删除动态路由以及保存的权限
b.代码
export function removeAsyncRouter() {
const permissionRouteList = store.state.menu.permissionRouteList
const removeRouteFn = (permissionRouteList) => {
if (permissionRouteList.length > 0) {
permissionRouteList.map(item => {
if (!item.component && item.children) return removeRouteFn(item.children)
router.removeRoute(item.name)
})
}
}
removeRouteFn(permissionRouteList)
store.commit('menu/setMenuList', [])
store.commit('menu/setPermissionCodeList', [])
}
d.在vuex当中将动态路由(+/静态路由)组合来渲染权限菜单展示
a.代码
import { loadAuthority } from '@/api/loaddata'
import { systemChildRouter } from '@/router/system'
const state = () => ({
permissionCodeList: [],
permissionRouteList: [],
menuList: [],
})
const mutations = {
setPermissionCodeList(state, permissionCodeList) {
state.permissionCodeList = permissionCodeList
},
setPermissionRouteList(state, permissionRouteList) {
state.permissionRouteList = permissionRouteList
},
setMenuList(state, permissionRouteList) {
const menuList = [...systemChildRouter, ...permissionRouteList]
},
}
const actions = {
async MenuInfo(ctx) {
const res = await loadAuthority.getUserAuthList({ userName: localStorage.getItem('userName') })
ctx.commit('setPermissionCodeList', res)
return res
}
}
export default {
namespaced: true,
state,
actions,
mutations
}
e.渲染菜单
a.说明
在vue组件计算属性当中获取vuex中组合好的渲染菜单的路由, 在模板中循环渲染展示菜单
b.代码
<template>
<a-layout-sider :width="220">
<a-menu mode="inline" v-model:selectedKeys="current">
<left-menu-item v-for="item in menuList" :key="item.path" :item="item">
</left-menu-item>
</a-menu>
</a-layout-sider>
</template>
<script>
import LeftMenuItem from './LeftMenuItem.vue'
export default {
name: "SystemMenu",
components: {
LeftMenuItem
},
data() {
return
current: ["help"],
};
},
computed: {
menuList() {
return this.$store.state.menu.menuList
}
},
};
</script>
<template>
<div>
<template v-if="!item.children">
<a-menu-item :key="item.path">
<template #icon>
<img :src="item.meta.img">
</template>
<router-link :to="item.path">{{ item.meta.title }}</router-link>
</a-menu-item>
</template>
<template v-else>
<a-sub-menu :key="item.path">
<template #icon>
<img :src="item.meta.img">
</template>
<template #title>{{ item.meta.title }}</template>
<left-menu-item v-for="item in item.children" :key="item.path" :item="item">
</left-menu-item>
</a-sub-menu>
</template>
</div>
</template>
<script>
export default {
name: "LeftMenuItem",
props: {
item: {
type: Object,
required: true
},
}
}
</script>
<style lang="scss" scoped></style>
c.递归自身组件
想要递归自身组件,需要在组件脚本中导出定义自身的name:LeftMenuItem
在组件内容即可调用自身的name来实现递归调用组件本身
这样就实现了菜单的权限控制,拥有不同权限的用户登录之后,就只会看到属于自己权限的页面!
02.实战场景2:权限控制 + 动态路由实战
a.定义动态路由
---
// router/system.js - 只有权限用户才能看到
export const permissionRoutes = [
{
path: 'user',
name: 'UserManage',
component: () => import('@/views/UserManage'),
meta: {
title: '用户管理',
permissionCode: 10004 // 权限标识
}
},
{
path: 'role',
name: 'RoleManage',
component: () => import('@/views/RoleManage'),
meta: {
title: '角色管理',
permissionCode: 10104
}
}
]
---
b.在beforeEach中处理权限
---
// router/permission.js
import router from '@/router'
import store from '@/store'
import { permissionRoutes } from '@/router/system'
let routeFlag = false // 防止重复处理
router.beforeEach(async (to, from, next) => {
const token = localStorage.getItem('token')
// 1. 未登录的用户只能访问登录页
if (!token) {
if (to.path === '/login') {
return true // 允许进入登录页
} else {
return '/login' // 重定向到登录页
}
}
// 2. 已登录用户不能再进登录页
if (to.path === '/login') {
return '/home' // 重定向回首页
}
// 3. 第一次登录时获取权限并添加动态路由
if (!routeFlag) {
routeFlag = true
// 获取用户权限列表
const userPermissions = await store.dispatch('auth/getUserPermissions')
// 3a. 筛选用户拥有的路由(核心:filter过滤)
const userRoutes = permissionRoutes.filter(route =>
userPermissions.includes(route.meta.permissionCode)
)
// 3b. 递归添加所有路由(包括嵌套子路由)
addRoutesRecursively(userRoutes)
// 3c. 添加404路由(必须在最后!)
router.addRoute({
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/NotFound')
})
// 3d. 保存权限路由到store(用于菜单渲染)
store.commit('auth/setPermissionRoutes', userRoutes)
// 3e. 重新进入当前路由(让新路由规则生效)
return to.fullPath
}
return true
})
// 递归函数:添加所有路由(包括嵌套的子路由)
function addRoutesRecursively(routes, parentName = '系统功能') {
routes.forEach(route => {
// 如果有component,说明是真实路由(不是单纯的分组)
if (route.component) {
router.addRoute(parentName, route) // 添加到指定的parent路由中
}
// 如果有子路由,递归处理
if (route.children && route.children.length) {
addRoutesRecursively(route.children, route.name)
}
})
}
---
c.登出时删除动态路由
---
// store/auth.js 中的logout action
export default {
actions: {
logout({ state, commit }) {
// 删除所有权限路由
const permissionRoutes = state.permissionRoutes
function removeRoutesRecursively(routes) {
routes.forEach(route => {
// 调用router.removeRoute()按名称删除
router.removeRoute(route.name)
// 如果有子路由,递归删除
if (route.children?.length) {
removeRoutesRecursively(route.children)
}
})
}
removeRoutesRecursively(permissionRoutes)
// 清空权限数据
commit('setPermissionRoutes', [])
commit('setUserPermissions', [])
// 清空token和用户信息
localStorage.removeItem('token')
localStorage.removeItem('user')
// 重定向到登录页
router.push('/login')
}
}
}
---
d.在菜单中渲染权限路由
---
<!-- components/Menu.vue -->
<template>
<aside>
<nav>
<!-- 循环渲染权限路由 -->
<menu-item
v-for="route in permissionRoutes"
:key="route.path"
:route="route"
/>
</nav>
</aside>
</template>
<script>
import { computed } from 'vue'
import { useStore } from 'vuex'
import MenuItem from './MenuItem.vue'
export default {
components: { MenuItem },
setup() {
const store = useStore()
// 从store中获取权限路由
const permissionRoutes = computed(() =>
store.state.auth.permissionRoutes
)
return { permissionRoutes }
}
}
</script>
---
---
<!-- components/MenuItem.vue - 递归组件 -->
<template>
<div>
<!-- 无子路由:直接显示为链接 -->
<template v-if="!route.children?.length">
<router-link :to="route.path">
<img :src="route.meta.img" />
{{ route.meta.title }}
</router-link>
</template>
<!-- 有子路由:显示为分组,递归调用自己 -->
<template v-else>
<div class="menu-group">
<p>{{ route.meta.title }}</p>
<menu-item
v-for="child in route.children"
:key="child.path"
:route="child"
/>
</div>
</template>
</div>
</template>
<script>
export default {
name: 'MenuItem', // 必须有name才能递归调用自己
props: {
route: {
type: Object,
required: true
}
}
}
</script>
---
2.5 常见问题:3类
01.汇总
错误1:无限重定向
错误2:路由刷新后权限丢失
错误3:异步操作未等待
02.常见问题:3类
a.错误1:无限重定向
// 错误
router.beforeEach((to, from, next) => {
if (!token) {
next('/login') // 重定向到登录页
}
})
// 问题:登录页面也会触发beforeEach,又重定向,形成死循环
// 正确
router.beforeEach((to, from, next) => {
if (!token && to.path !== '/login') {
return '/login' // 只有非登录页才重定向
}
})
b.错误2:路由刷新后权限丢失
// 问题代码
router.beforeEach(async (to, from, next) => {
// 第一次加载时添加动态路由
if (!routeFlag) {
routeFlag = true
// ... 添加路由
}
})
// 问题:routeFlag是本地变量,刷新后会重置
// 正确做法
// 使用全局flag而不是本地变量
const globalState = {
routesAdded: false
}
router.beforeEach(async (to, from, next) => {
if (!globalState.routesAdded) {
globalState.routesAdded = true
// ... 添加路由
}
})
// 或者更好:用store管理
const store = useStore()
if (!store.state.routesAdded) {
store.commit('setRoutesAdded', true)
// ... 添加路由
}
c.错误3:异步操作未等待
// 错误
router.beforeEach((to, from, next) => {
store.dispatch('getUserPermissions') // 没有等待
next() // 立即放行,权限还未加载
})
// 正确
router.beforeEach(async (to, from, next) => {
const permissions = await store.dispatch('getUserPermissions') // 等待
addDynamicRoutes(permissions)
next()
})
// 或直接返回(Vue Router会自动等待Promise)
router.beforeEach(async (to, from) => {
const permissions = await store.dispatch('getUserPermissions')
addDynamicRoutes(permissions)
// 返回undefined表示允许导航
})
3 channel
3.1 汇总:8种
01.常用信息
a.速查表
通讯方式 通讯方向 实现方式 学习成本 推荐度 何时用
1️⃣ Props+Emit 父→子→父 属性绑定 + 事件 ⭐ 低 ⭐⭐⭐⭐⭐ 简单父子通讯
2️⃣ Ref+defineExpose 父→子 组件实例引用 ⭐⭐ 中低 ⭐⭐⭐⭐ 调用子组件方法
3️⃣ $attrs+$inheritAttrs 多层透传 属性穿透 ⭐⭐ 中低 ⭐⭐⭐ 多层嵌套传值
4️⃣ Provide/Inject 祖先→后代 上下文传递 ⭐⭐ 中低 ⭐⭐⭐ 深层嵌套组件
5️⃣ Slots作用域插槽 父←→子 双向数据流 ⭐⭐ 中低 ⭐⭐⭐⭐ 灵活内容分发
6️⃣ mitt事件总线 任意组件 发布-订阅 ⭐⭐⭐ 中 ⭐⭐⭐ 兄弟/远程通讯
7️⃣ Pinia/Vuex 全局共享 状态管理库 ⭐⭐⭐⭐ 高 ⭐⭐⭐⭐ 大型应用状态
8️⃣ $parent/$children 直接访问 实例属性 ⭐ 低 ⭐ 紧急情况
b.性能排序(从快到慢)
1️⃣ Props+Emit ← 最快(直接属性绑定) 官方标准,父子最优方案,数据流单向清晰 父子Props+Emit,双向用v-model
2️⃣ Ref+defineExpose ← 快(直接实例引用) 父组件直接调子组件方法,但破坏封装性 特殊Ref和Slots,插槽最优雅
3️⃣ $attrs+$inheritAttrs ← 快(属性穿透) 多层嵌套时的属性穿透工具,简化中间层处理 多层$attrs穿
4️⃣ Provide/Inject ← 中等(上下文查询) 跨层级传值的快捷方式,但只能祖先→后代 深层Provide+Inject
5️⃣ Slots作用域插槽 ← 中等(函数调用) 父子双向数据流最优雅的方式,高度灵活 特殊Ref和Slots,插槽最优雅
6️⃣ mitt事件总线 ← 中偏慢(事件映射) 兄弟通讯的解耦方案,但需手动管理生命周期 兄弟mitt(轻)或Pinia(重),全局管状态
7️⃣ Pinia/Vuex ← 偏慢(响应式系统) 全局状态管理,适合跨越任意层级的复杂通讯 兄弟mitt(轻)或Pinia(重),全局管状态
8️⃣ $parent/$children ← 最慢(树遍历) 紧急逃生舱,强耦合禁用 避免$parent用,强耦合是坑
c.JS知识点对比
方式 核心JS特性 原理 风险
1️⃣ Props+Emit Object.defineProperty / Proxy 属性绑定+事件系统 ✅ 低
2️⃣ Ref+defineExpose 闭包、组件实例 暴露组件内部 ⚠️ 中(破坏封装)
3️⃣ $attrs+$inheritAttrs 属性穿透、Object.assign 跳过中间层 ✅ 低
4️⃣ Provide/Inject 上下文、闭包 祖先传递、后代注入 ⚠️ 中(流向不透明)
5️⃣ Slots作用域插槽 函数、作用域链 父组件提供数据给插槽 ✅ 低
6️⃣ mitt事件总线 发布-订阅、EventTarget 中央事件管理 ⚠️ 中(难以追踪)
7️⃣ Pinia/Vuex Reactive/Ref、单例 全局状态存储 ⚠️ 中(复杂度高)
8️⃣ $parent/$children 实例树、直接引用 直接访问组件树 ❌ 高(强耦合)
d.快速对比
方式 父子 兄弟 跨层 全局 类型安全 推荐度
1️⃣ Props+Emit ✅✅✅ ❌ ❌ ❌ ✅✅✅ ⭐⭐⭐⭐⭐
2️⃣ Ref+defineExpose ✅✅ ❌ ❌ ❌ ✅✅✅ ⭐⭐⭐⭐
3️⃣ $attrs+$inheritAttrs ✅✅ ❌ ✅✅ ❌ ⚠️ ⭐⭐⭐
4️⃣ Provide/Inject ✅ ❌ ✅✅✅ ⚠️ ⚠️ ⭐⭐⭐
5️⃣ Slots作用域插槽 ✅✅ ❌ ❌ ❌ ✅✅ ⭐⭐⭐⭐
6️⃣ mitt事件总线 ⚠️ ✅✅✅ ✅ ✅ ⚠️ ⭐⭐⭐⭐
7️⃣ Pinia/Vuex ✅ ✅✅✅ ✅✅✅ ✅✅✅ ✅✅✅ ⭐⭐⭐⭐⭐
8️⃣ $parent/$children ✅ ❌ ⚠️ ❌ ❌ ⭐
e.对比方式
props 父->子组件:通过【props】传递数据的
$emit 子->父组件:通过【$emit触发事件】传递数据的
-----------------------------------------------------------------------------------------------------
$parent 获取当前组件的父组件
$children 获取当前组件的子组件
-----------------------------------------------------------------------------------------------------
$attrs 多层$attrs穿
-----------------------------------------------------------------------------------------------------
深层Provide+Inject 父组件中通过【provide】来【定义变量】
深层Provide+Inject 在子组件中通过【inject】来【使用变量】
-----------------------------------------------------------------------------------------------------
$refs 获取组件实例
pinia 一般超过两个组件交互用pinia
f.实战场景
实战场景 推荐方案 备选方案 不推荐
表单父子数据绑定 v-model Props+Emit Pinia
表格搜索、分页 Props+Emit $attrs Provide/Inject
弹窗打开/关闭 v-model Ref+defineExpose mitt
调用子组件方法 Ref+defineExpose Props回调 Provide
多层表单验证 Provide/Inject Props逐层 $parent
兄弟表单联动 Pinia mitt Provide/Inject
Tab切换标签页 Props+Emit Pinia mitt
主题切换(全局) Pinia Provide/Inject mitt
插件扩展功能 Props + Slots Provide/Inject defineExpose
02.通讯方向流程图
a.父子通讯(最常用)
┌─────────────────────────────────────────────────────────────┐
│ 父子通讯(最常用) │
├─────────────────────────────────────────────────────────────┤
│ │
│ 父组件 子组件 │
│ ────────────Props────────→ │
│ (defineProps接收) │
│ ←────────Emit/update──── │
│ (使用 @eventName 监听) (defineEmits触发) │
│ │
│ 或双向:v-model │
│ ───────── v-model:msg ────→ │
│ ←──── emit('update:msg') ──── │
│ │
└─────────────────────────────────────────────────────────────┘
b.父子引用(调用子组件方法)
┌─────────────────────────────────────────────────────────────┐
│ 父子引用(调用子组件方法) │
├─────────────────────────────────────────────────────────────┤
│ │
│ 父组件 子组件 │
│ ← ref="child" → │
│ child.value.method() defineExpose({method}) │
│ │
└─────────────────────────────────────────────────────────────┘
c.跨层级通讯(祖先→后代)
┌─────────────────────────────────────────────────────────────┐
│ 跨层级通讯(祖先→后代) │
├─────────────────────────────────────────────────────────────┤
│ │
│ 祖先组件 │
│ provide('key', value) │
│ ↓↓↓ (跨越中间组件) │
│ 后代组件 │
│ inject('key') │
│ │
│ ⚠️ 注意:祖先→后代单向,无法反向! │
│ │
└─────────────────────────────────────────────────────────────┘
d.兄弟通讯(不直接相连)
┌─────────────────────────────────────────────────────────────┐
│ 兄弟通讯(不直接相连) │
├─────────────────────────────────────────────────────────────┤
│ │
│ 兄弟A 事件总线/Pinia 兄弟B │
│ emit('event') ←────────→ listen('event') │ │
│ │
│ 或通过父组件中转: │
│ 子A @event → 父 :prop → 子B │
│ │
└─────────────────────────────────────────────────────────────┘
03.选型决策树(重点!)
a.问题:我需要在两个组件间通讯
├─ Q1: 是父子组件吗?
│ ├─ YES → 用 Props + Emit ⭐⭐⭐⭐⭐
│ │ └─ 需要双向绑定?→ 用 v-model
│ │ └─ 需要调用子方法?→ 用 Ref + defineExpose
│ │
│ └─ NO → Q2
├─ Q2: 是深层嵌套吗?(祖父→孙子)
│ ├─ YES → Provide/Inject ⭐⭐⭐⭐
│ │ └─ 但避免双向通讯!
│ │
│ └─ NO → Q3
├─ Q3: 是兄弟组件吗?
│ ├─ YES → Q4
│ │
│ └─ NO → Q5
├─ Q4: 组件关系复杂吗?
│ ├─ 简单(仅几个兄弟) → 事件总线 mitt ⭐⭐⭐⭐
│ ├─ 复杂(多个兄弟交互) → Pinia ⭐⭐⭐⭐⭐
│ └─ 临时通讯 → 通过父组件转发 Props+Emit
│
├─ Q5: 是全局状态吗?
│ ├─ YES → Pinia/Vuex ⭐⭐⭐⭐⭐
│ └─ NO → 使用作用域插槽 Slots ⭐⭐⭐
结论:
✅ 最优先:Props + Emit(最清晰)
✅ 次优先:Pinia(跨越层级)
⚠️ 谨慎用:Provide/Inject(单向限制)
❌ 不推荐:$parent/$children(强耦合)
b.问题:我需要通讯
├─ 父子?
│ ├─ 单向传值 → Props ✅
│ ├─ 子传父 → Emit ✅
│ ├─ 双向绑定 → v-model ✅
│ └─ 调用方法 → Ref+defineExpose ⭐
│
├─ 兄弟组件?
│ ├─ 2-3个 → mitt事件总线 ✅
│ └─ 多个+复杂 → Pinia ✅✅
│
├─ 深层嵌套?
│ ├─ 3层+ → Provide/Inject ✅
│ └─ 2层 → Props逐层 ✅
│
├─ 多层穿透?
│ └─ 属性透传 → $attrs ✅
│
└─ 全局状态?
└─ Pinia ✅✅✅
c.常见错误速览
a.分类1
❌ 错误1:直接修改Props
props.count = 10 // 控制台会警告!
✅ 正确:用emit通知父组件
emit('update:count', 10)
b.分类2
❌ 错误2:Provide/Inject反向通讯
孙组件想通知祖先 → 无法直接用inject实现
✅ 正确:配合emit或callback
provide('onUpdate', (data) => {})
c.分类3
❌ 错误3:事件总线内存泄漏
mounted() {
bus.on('event', handler) // 未清理!
}
✅ 正确:及时卸载
onUnmounted(() => {
bus.off('event', handler)
})
d.分类4
❌ 错误4:过度使用defineExpose
暴露所有方法和数据 → 组件失去黑盒特性
✅ 正确:只暴露必要的API
defineExpose({ search, reset }) // 仅必要
e.分类5
❌ 错误5:混乱的数据流
多种通讯方式混用 → 难以维护
✅ 正确:统一规范
项目内只用 Props+Emit+Pinia 三剑客
3.2 [1]父子通讯(Props+Emit)
00.汇总
a.核心概念速查表
概念 定义 JS原理 何时用
Props 父→子的单向数据流 Object.defineProperty / Proxy 父组件主动传值
Emit 子→父的事件触发 发布-订阅模式 子组件通知父组件
defineProps 声明接收的属性 编译时宏 Composition API
defineEmits 声明触发的事件 编译时宏 Composition API
v-model 双向数据绑定 Props+Emit组合 表单双向绑定
$event 事件回调的参数 原生事件对象 接收emitted数据
b.Props + Emit 的 JS 知识点
JS特性 在Props中的应用 在Emit中的应用 原理
闭包 组件内私有props变量 保存emit回调 作用域隔离
Object.defineProperty Vue2的Props响应式 事件系统 属性拦截
Proxy Vue3的Props响应式 事件代理 全面拦截
事件系统 Props验证时 Emit触发机制 发布-订阅
单向数据流 Props不可直接修改 只能通过emit改变 数据可追踪
作用域链 this指向问题 函数上下文 词法作用域
c.一句话总结
Props是父→子的单向属性传递,Emit是子→父的事件通知,v-model是两者的语法糖,组成Vue最基础也最重要的通讯机制。
d.快速参考:Props + Emit 完整流程
【场景】:表单子组件,父组件修改值
父组件:
├─ <Child v-model="msg" />
└─ 期望:msg 能被子组件修改
子组件:
├─ defineProps({ modelValue: String })
├─ defineEmits(['update:modelValue'])
├─ @input="$emit('update:modelValue', $event.target.value)"
└─ 完成!
---
【场景】:按钮组件,子组件通知父组件
子组件:
├─ @click="$emit('click')"
父组件:
├─ <Button @click="handleClick" />
└─ 完成!
01.作用域与 this 问题详解
a.Options API 中的 this
a.代码
export default {
props: {
msg: String
},
data() {
return {
count: 0
}
},
methods: {
test() {
console.log(this.msg) // ✅ 可以访问props
console.log(this.count) // ✅ 可以访问data
}
},
computed: {
doubled() {
return this.count * 2 // ✅ 可以访问
}
}
}
b.规则
Options API中的methods、computed、watch都可以用this访问props/data
b.Composition API 中无this
a.代码
<script setup>
// ❌ 这里没有 this
console.log(this) // undefined
// ✅ 正确做法
const props = defineProps({
msg: String
})
const emit = defineEmits(['update'])
const count = ref(0)
const test = () => {
console.log(props.msg) // ✅ 直接访问props
console.log(count.value) // ✅ ref数据用.value
}
</script>
b.关键
Composition API 中完全没有 this
Props 直接赋给变量访问
ref/reactive 数据需要 .value 访问
c.箭头函数中的 this 问题
a.代码
export default {
props: {
msg: String
},
methods: {
// ❌ 错误:箭头函数中的this指向全局window
test1: () => {
console.log(this.msg) // undefined,this是window
},
// ✅ 正确:普通函数中的this指向组件实例
test2() {
console.log(this.msg) // 可以访问props
}
}
}
b.规则
Options API中不要用箭头函数定义methods
02.Props + Emit 最佳实践
a.Props 类型检查的完整示例
// 完整的验证示例
defineProps({
// 基础类型
title: String,
count: Number,
active: Boolean,
// 多种类型
size: [String, Number],
// 有默认值
status: {
type: String,
default: 'pending'
},
// 必传
userId: {
type: Number,
required: true
},
// 自定义验证
email: {
type: String,
validator: (value) => {
return /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/.test(value)
}
},
// 工厂函数默认值(用于Array/Object)
tags: {
type: Array,
default: () => [],
validator: (arr) => Array.isArray(arr)
},
// 引用类型验证
user: {
type: Object,
default: () => ({ name: 'Guest' }),
validator: (obj) => obj.hasOwnProperty('name')
}
})
b.Emit 事件命名规范
// ✅ 推荐:kebab-case(短横线)
emit('update-user', newUser)
@update-user="handleUpdate"
// ❌ 避免:camelCase
emit('updateUser', newUser)
// ✅ 特殊名称:update:PropName
emit('update:count', newCount)
v-model:count
// ✅ 业务事件
emit('confirm')
emit('cancel')
emit('open')
emit('close')
c.Props + Emit 最佳实践
a.推荐
// 1. Props用于"父组件的意图"
// 2. Emit用于"子组件的反馈"
// 示例:搜索组件
const props = defineProps({
query: String, // 父组件搜索词
loading: Boolean // 父组件加载状态
})
const emit = defineEmits(['update:query', 'search'])
const handleSearch = () => {
emit('update:query', inputValue.value)
emit('search', inputValue.value)
}
b.避免
// 避免:子组件独立改变props
props.query = newValue // ❌
// 避免:props name过于通用
defineProps(['data']) // ❌ 太模糊
// 避免:过多的emit
emit('update:a')
emit('update:b')
emit('update:c') // ❌ 改用v-model:multiple
3.3 [1]父传子通信(Props)
01.父传子通信(Props)
a.概述
父传子通信是Vue中最基本也是最常用的通信方式。
父组件通过属性(props)向子组件传递数据,子组件通过defineProps接收并使用这些数据。
这是单向数据流,子组件不能直接修改props,确保数据的可追踪性。
b.完整代码示例
a.父组件(ParentComponent.vue)
<template>
<div>
<h2>父组件</h2>
<p>父组件的消息: {{ parentMessage }}</p>
<child-component
:message="parentMessage"
:userInfo="userInfo"
:count="count"
/>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import ChildComponent from './ChildComponent.vue'
const parentMessage = ref('Hello from Parent')
const userInfo = reactive({ name: 'John', age: 30, role: 'admin' })
const count = ref(0)
</script>
b.子组件(ChildComponent.vue)
<template>
<div class="child">
<h3>子组件</h3>
<p>接收的消息: {{ message }}</p>
<p>用户名: {{ userInfo.name }}</p>
<p>用户角色: {{ userInfo.role }}</p>
<p>计数: {{ count }}</p>
</div>
</template>
<script setup>
import { defineProps } from 'vue'
const props = defineProps({
message: String,
userInfo: Object,
count: {
type: Number,
default: 0
}
})
// 在script中访问props
console.log(props.message) // 'Hello from Parent'
</script>
c.说明
父组件通过 : 或 v-bind 将数据绑定到子组件的属性上
子组件使用 defineProps 声明接收的属性
在模板中可以直接使用属性名(不需要 props.),在script中需要用 props. 前缀访问
若要修改父组件的值,需要子组件通过emit事件通知父组件
02.Props的5种定义方式
a.方式1:简单数组形式
a.代码
// 子组件
defineProps(['msg', 'count', 'user'])
// 父组件
<Child :msg="'hello'" :count="10" :user="{name: 'John'}" />
b.说明
优点:简单快速
不足:无类型检查、无默认值、无验证
b.方式2:对象形式(推荐)
a.代码
// 子组件
defineProps({
msg: String,
count: Number,
user: Object
})
// 使用
<script setup>
const props = defineProps({
msg: String,
count: Number,
user: Object
})
console.log(props.msg) // ✅ 可以直接访问
</script>
<template>
<div>{{ msg }}</div> <!-- ✅ 在template中可省略props. -->
</template>
b.说明
优点:有类型检查、代码清晰
特点:Vue会自动进行类型检查(开发环境)
c.方式3:完整的验证形式
a.代码
// 子组件
defineProps({
// 基础验证
msg: {
type: String,
required: true
},
// 有默认值
count: {
type: Number,
default: 0
},
// 自定义验证
email: {
type: String,
validator(value) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
}
},
// 工厂函数默认值(对象/数组)
tags: {
type: Array,
default: () => ['vue', 'javascript']
},
// 多种类型
size: {
type: [Number, String],
default: 'medium'
}
})
b.说明
`required: true` 表示必传
`default` 是默认值(基础类型直接赋值,引用类型用函数)
`validator` 自定义验证规则
d.方式4:TypeScript类型定义
a.代码
// 子组件 - TypeScript
interface User {
id: number
name: string
age?: number
}
const props = defineProps<{
msg: string
count: number
user: User
tags?: string[] // 可选
}>()
// 有默认值需要用withDefaults
const props = withDefaults(defineProps<{
count: number
size?: 'small' | 'medium' | 'large'
}>(), {
count: 10,
size: 'medium'
})
b.说明
优点:完整的类型检查和IDE智能提示
缺点:需要TypeScript环境
e.方式5:使用宏定义(最现代)
a.代码
// Vue3.3+ 的最简形式
<script setup lang="ts">
// 直接在模板中使用,自动推断
defineProps<{
msg: string
count?: number
user?: { name: string }
}>()
</script>
<template>
<div>{{ msg }}</div>
</template>
03.Props常见错误与解决
a.错误1:直接修改Props
// ❌ 错误
const props = defineProps({
count: Number
})
props.count = 10 // Vue会警告!且不会更新父组件
// ✅ 正确做法1:如果想修改父组件的值
const emit = defineEmits(['update:count'])
const increment = () => {
emit('update:count', props.count + 1) // 通知父组件
}
// ✅ 正确做法2:子组件有独立的状态
import { ref, computed } from 'vue'
const props = defineProps({
modelValue: Number
})
const localCount = ref(props.modelValue)
// 监听父组件传来的值变化
watch(() => props.modelValue, (newVal) => {
localCount.value = newVal
})
-----------------------------------------------------------------------------------------------------
原因:Props是单向数据流,改动父组件状态应该通过emit
b.错误2:对象/数组Props的浅拷贝陷阱
// ❌ 危险
const props = defineProps({
user: Object
})
// 直接修改嵌套属性,父组件能看到
props.user.name = 'New Name' // ⚠️ 子组件改了父组件的值!
// ✅ 正确:用emit通知
const emit = defineEmits(['update:user'])
const changeName = (newName) => {
emit('update:user', { ...props.user, name: newName })
}
-----------------------------------------------------------------------------------------------------
原因:Object/Array是引用类型,直接修改会影响父组件
c.错误3:Props中使用$和this
// ❌ 错误(Options API)
export default {
props: {
msg: String
},
methods: {
test() {
console.log(this.msg) // ✅ 这里可以用this
}
}
}
// ❌ 错误(Composition API - 最常见的错误)
<script setup>
defineProps({
msg: String
})
// ❌ 不要这样!
console.log(this.msg) // 错误!setup中无this
// ✅ 正确
const props = defineProps({
msg: String
})
console.log(props.msg) // 直接访问
</script>
-----------------------------------------------------------------------------------------------------
关键:
Composition API(<script setup>)中:没有this,直接用const props = defineProps()
Options API中:可以用this.propName访问
3.4 [1]子传父通信(Emit)
01.子传父通信(Emit)
a.概述
子传父通信通过触发自定义事件实现。子组件在需要向父组件传递数据时,通过defineEmits定义事件,然后使用emit方法触发该事件并传递数据。父组件通过@事件名监听这个事件并处理回调。
b.完整代码示例
a.子组件(ChildComponent.vue)
<template>
<div class="child">
<h3>子组件</h3>
<button @click="sendData">发送数据给父组件</button>
<button @click="notifyDelete">通知父组件删除</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const childMessage = ref('Hello from Child')
// 定义子组件能触发的事件
const emit = defineEmits(['update', 'delete', 'custom-event'])
const sendData = () => {
// 触发update事件,传递数据
emit('update', {
id: 1,
message: childMessage.value,
timestamp: new Date()
})
}
const notifyDelete = () => {
// 触发delete事件
emit('delete', { id: 1 })
}
</script>
b.父组件(ParentComponent.vue)
<template>
<div>
<h2>父组件</h2>
<p v-if="childData">子组件发来的数据: {{ childData.message }}</p>
<child-component
@update="handleUpdate"
@delete="handleDelete"
@custom-event="handleCustom"
/>
</div>
</template>
<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
const childData = ref(null)
// 处理子组件的update事件
const handleUpdate = (data) => {
console.log('收到子组件数据:', data)
childData.value = data
}
// 处理子组件的delete事件
const handleDelete = (payload) => {
console.log('子组件通知删除:', payload.id)
}
// 处理自定义事件
const handleCustom = (data) => {
console.log('自定义事件:', data)
}
</script>
c.说明
子组件通过 defineEmits 声明可以触发的事件
使用 emit('eventName', data) 触发事件并传递数据
父组件通过 @eventName="handler" 监听事件
事件名建议使用 kebab-case(短横线)命名
$event 参数接收emit传递的数据
02.Emit的3种定义方式
a.方式1:简单数组
const emit = defineEmits(['update', 'delete'])
emit('update', newData)
b.方式2:对象形式(推荐有验证)
const emit = defineEmits({
// 不需要验证
update: null,
// 需要验证(接收函数形式的validator)
customEvent: (payload) => {
if (typeof payload !== 'object') {
console.warn('payload必须是对象')
return false
}
return true
}
})
// 使用
emit('update', newData)
emit('customEvent', { id: 1 }) // 会经过validator验证
c.方式3:TypeScript 类型定义
const emit = defineEmits<{
update: [value: string] // 一个参数
delete: [id: number] // 一个参数
multi: [x: number, y: number] // 多个参数
custom: [payload: { name: string }] // 对象参数
}>()
// 使用
emit('update', 'hello')
emit('multi', 1, 2)
emit('custom', { name: 'John' })
---
优点:完整的类型检查
3.5 [1]双向数据绑定(v-model)
01.双向数据绑定(v-model)
a.概述
v-model是Props和Emit的语法糖,用于实现父子组件的双向数据绑定。
父组件的数据改变时,子组件自动更新;子组件修改时,父组件的数据也自动同步。
使用v-model使代码更简洁,特别适合表单组件。
b.完整代码示例
a.子组件(FormInput.vue)
<template>
<div>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
placeholder="输入内容"
/>
<p>输入内容: {{ modelValue }}</p>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue'
// 接收modelValue prop
const props = defineProps({
modelValue: {
type: String,
required: true
}
})
// 定义update:modelValue事件
const emit = defineEmits(['update:modelValue'])
</script>
b.父组件(ParentComponent.vue)
<template>
<div>
<h2>父组件</h2>
<p>当前值: {{ count }}</p>
<!-- ✅ 使用v-model,简洁优雅 -->
<FormInput v-model="count" />
<!-- 等同于下面的写法 -->
<!-- <FormInput :modelValue="count" @update:modelValue="count = $event" /> -->
</div>
</template>
<script setup>
import { ref } from 'vue'
import FormInput from './FormInput.vue'
const count = ref('')
</script>
c.多个v-model示例
<!-- 父组件 -->
<template>
<Form
v-model:title="title"
v-model:description="description"
v-model:tags="tags"
/>
</template>
<script setup>
import { ref } from 'vue'
import Form from './Form.vue'
const title = ref('')
const description = ref('')
const tags = ref([])
</script>
<!-- 子组件 Form.vue -->
<script setup>
defineProps({
title: String,
description: String,
tags: Array
})
defineEmits(['update:title', 'update:description', 'update:tags'])
</script>
<template>
<div>
<input
:value="title"
@input="$emit('update:title', $event.target.value)"
/>
<textarea
:value="description"
@input="$emit('update:description', $event.target.value)"
/>
<input
:value="tags.join(',')"
@input="$emit('update:tags', $event.target.value.split(','))"
/>
</div>
</template>
c.说明
v-model本质是 :modelValue + @update:modelValue 的组合
子组件必须接收 modelValue 属性,发送 update:modelValue 事件
可以用多个v-model绑定不同的属性(v-model:propName)
v-model大大简化了表单类组件的开发
在input、select等原生表单元素上也可以用v-model
02.v-model 的原理与使用
a.基础:v-model = Props + Emit 的语法糖
// 父组件使用
<Child v-model="msg" />
// 等同于
<Child :modelValue="msg" @update:modelValue="msg = $event" />
-----------------------------------------------------------------------------------------------------
展开式:
父组件通过 `modelValue` props 传入值
子组件通过 `@update:modelValue` emit 修改值
`$event` 是emit传过来的新值
b.子组件实现 v-model
// 子组件
const props = defineProps({
modelValue: String // ✅ 必须叫 modelValue
})
const emit = defineEmits(['update:modelValue']) // ✅ 必须叫 update:modelValue
const updateValue = (newVal) => {
emit('update:modelValue', newVal)
}
c.父组件使用
<template>
<Child v-model="count" />
<!-- 等同于 -->
<Child :modelValue="count" @update:modelValue="count = $event" />
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
03.多个 v-model(重点!)
a.说明
v-model 可以指定绑定的属性名!
b.子组件
// 子组件
const props = defineProps({
title: String, // 用于 v-model:title
description: String // 用于 v-model:description
})
const emit = defineEmits(['update:title', 'update:description'])
const updateTitle = (newVal) => {
emit('update:title', newVal)
}
c.父组件
<!-- 父组件 -->
<template>
<Child
v-model:title="formData.title"
v-model:description="formData.description"
/>
</template>
<script setup>
import { reactive } from 'vue'
const formData = reactive({
title: '标题',
description: '描述'
})
</script>
3.6 [2]父组件调用子组件方法(Ref + defineExpose)
00.汇总
a.核心概念速查表
概念 定义 返回值 何时用 风险
模板ref 对DOM元素的引用 DOM元素 直接操作DOM 破坏结构
组件ref 对组件实例的引用 组件实例 调用子方法 强耦合
defineExpose 暴露组件内部API 对象 开放特定方法 打破封装
$nextTick 等待DOM更新 Promise 异步访问ref 依赖异步
?.操作符 安全链式访问 值或undefined 防空指针 需判断
b.Ref + defineExpose 的 JS 知识点
JS特性 在Ref中的应用 在defineExpose中的应用 原理
闭包 保存组件实例引用 私有变量暴露控制 作用域保护
引用类型 ref对象的.value 返回的是组件实例 指针指向
模板编译 v-ref指令 宏编译 AST转换
异步性 DOM更新延迟 组件挂载延迟 事件循环
WeakMap Vue内部实现 组件缓存 内存管理
Proxy 可能拦截 实例代理 元编程
c.一句话总结
Ref + defineExpose是父组件直接调用子组件方法的机制,但应谨慎使用以避免打破组件封装,仅在必要时(如控制子组件行为)才使用。
d.Ref vs Props + Emit(何时选用)
场景 推荐方案 原因
父组件传值给子 Props 清晰的数据流
子组件通知父组件 Emit 解耦的事件系统
调用子组件方法 Ref + defineExpose 直接调用,无需事件
获取子组件状态 Ref + defineExpose(谨慎)仅特殊场景
表单双向绑定 v-model 语法糖,清晰
操作子DOM Ref 必须用
e.决策树
需要调用多个相关方法?→ 用 Ref
只是传值和通知?→ 用 Props+Emit
01.父组件调用子组件方法(Ref + defineExpose)
a.概述
父组件可以通过ref获取子组件实例,然后调用子组件暴露的方法。
子组件通过defineExpose暴露特定的方法和属性给父组件。
这种方式用于需要直接控制子组件行为的场景,如搜索表单的重置、数据加载等。
b.完整代码示例
a.子组件(SearchForm.vue)
<template>
<div class="search-form">
<h3>搜索表单</h3>
<input v-model="searchQuery" placeholder="搜索关键词" />
<input v-model="category" placeholder="分类" />
<button @click="search">搜索</button>
<button @click="reset">重置</button>
<p v-if="results.length">找到 {{ results.length }} 条结果</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
const searchQuery = ref('')
const category = ref('')
const results = ref([])
// 搜索方法
const search = async () => {
// 模拟API调用
results.value = [
{ id: 1, title: 'Result 1' },
{ id: 2, title: 'Result 2' }
]
console.log('执行搜索:', searchQuery.value)
}
// 重置方法
const reset = () => {
searchQuery.value = ''
category.value = ''
results.value = []
console.log('表单已重置')
}
// 获取结果
const getResults = () => results.value
// ✅ 暴露给父组件的API
defineExpose({
search,
reset,
getResults
})
</script>
b.父组件(ParentComponent.vue)
<template>
<div>
<h2>父组件</h2>
<SearchForm ref="searchFormRef" />
<button @click="parentSearch">父组件触发搜索</button>
<button @click="parentReset">父组件重置表单</button>
<button @click="exportResults">导出结果</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
import SearchForm from './SearchForm.vue'
const searchFormRef = ref(null)
const parentSearch = () => {
// ✅ 调用子组件的search方法
searchFormRef.value?.search()
}
const parentReset = () => {
// ✅ 调用子组件的reset方法
searchFormRef.value?.reset()
}
const exportResults = () => {
// ✅ 调用子组件的getResults方法
const results = searchFormRef.value?.getResults()
console.log('导出结果:', results)
}
</script>
c.说明
父组件使用 ref="refName" 获取子组件实例引用
子组件使用 defineExpose({ method1, method2, ... }) 暴露需要的方法
使用 ?. 操作符安全访问ref(防止ref为null时的错误)
仅在必要时使用此方式,避免过度耦合
适合调用特定方法而不是传递数据
02.说明
a.什么是 defineExpose
// defineExpose 就是"告诉父组件,我哪些方法可以被调用"
---
<script setup>
import { ref } from 'vue'
// 私有变量
const counter = ref(0)
const privateMethod = () => { /* ... */ }
// 暴露给父组件的公开API
defineExpose({
counter, // 可以访问计数器
increment: () => counter.value++ // 可以调用增加
// privateMethod 没有暴露,父组件无法访问
})
</script>
---
核心思想:
不在 defineExpose 中的内容,父组件完全无法访问
defineExpose 是组件的"公开接口"(类似类的public方法)
b.模板Ref(对DOM的引用)
// 获取DOM元素
<script setup>
import { ref, onMounted } from 'vue'
const inputRef = ref(null) // ✅ 初始化为null
onMounted(() => {
// ✅ 此时DOM已挂载,ref有值
inputRef.value?.focus() // 安全访问
})
</script>
<template>
<input ref="inputRef" /> <!-- ✅ 绑定ref -->
</template>
-----------------------------------------------------------------------------------------------------
关键:
ref初始值是 null
在 onMounted 之后才能安全访问
使用 ?. 操作符防空指针错误
c.ref 在循环中的使用
// ❌ 不要这样(取不到数据)
---
<script setup>
import { ref } from 'vue'
const itemRefs = ref([])
// 循环中每项会push到数组
</script>
<template>
<div v-for="(item, i) in items" :key="i">
<p ref="itemRefs">{{ item }}</p> <!-- ❌ 不安全 -->
</div>
</template>
---
// ✅ 正确做法
---
<script setup>
import { ref } from 'vue'
const itemRefs = ref([])
const setItemRef = (el) => {
if (el) itemRefs.value.push(el)
}
</script>
<template>
<div v-for="(item, i) in items" :key="i">
<p :ref="setItemRef">{{ item }}</p> <!-- ✅ 函数式 -->
</div>
</template>
---
d.组件Ref(对组件实例的引用)
// 父组件
---
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const childRef = ref(null)
const callChildMethod = () => {
// ✅ 调用子组件暴露的方法
childRef.value?.search()
childRef.value?.reset()
}
</script>
<template>
<Child ref="childRef" />
<button @click="callChildMethod">调用子组件方法</button>
</template>
---
// 子组件 - 需要用defineExpose暴露
---
<script setup>
import { ref } from 'vue'
const searchInput = ref('')
const search = () => {
console.log('搜索:', searchInput.value)
}
const reset = () => {
searchInput.value = ''
}
// ✅ 必须暴露,父组件才能访问
defineExpose({
search,
reset
})
</script>
<template>
<input v-model="searchInput" />
</template>
03.安全隐患
a.隐患1:过度暴露
// ❌ 不要暴露所有
defineExpose({
state,
internalState,
privateMethod,
allMethods // 等等
// 这样子组件的所有逻辑都对父组件透明了
})
// ✅ 只暴露必要的
defineExpose({
search, // 父组件确实需要的
reset,
export: () => { /* ... */ }
})
b.隐患2:直接修改子组件状态
// 子组件暴露了state
defineExpose({ counter })
// 父组件直接修改
const childRef = ref(null)
childRef.value.counter.value = 999 // ⚠️ 子组件无法控制!
// ✅ 应该提供修改方法
defineExpose({
getCounter: () => counter.value,
setCounter: (val) => {
// 可以在这里加验证逻辑
if (val >= 0) counter.value = val
}
})
c.隐患3:异步访问的空指针
// ❌ 容易出错
const childRef = ref(null)
const test = () => {
// 第一次执行时childRef.value可能是null(组件未挂载)
childRef.value.search() // ❌ 报错!
}
-----------------------------------------------------------------------------------------------------
// ✅ 正确做法
const test = () => {
childRef.value?.search() // 使用?.操作符
// 或者
if (childRef.value) {
childRef.value.search()
}
}
-----------------------------------------------------------------------------------------------------
// ✅ 或用nextTick确保DOM已准备
import { nextTick } from 'vue'
await nextTick() // 等待DOM更新
childRef.value?.search()
04.TypeScript 中的类型定义
a.基础类型定义
// 子组件
<script setup lang="ts">
import { ref } from 'vue'
const counter = ref(0)
const search = (query: string): Promise<any[]> => {
// 实现...
return Promise.resolve([])
}
const reset = (): void => {
counter.value = 0
}
// 定义暴露的类型
defineExpose({
counter,
search,
reset
})
</script>
b.使用 ref 时的类型注解
// 父组件
<script setup lang="ts">
import { ref } from 'vue'
import type { InstanceType } from 'vue'
import Child from './Child.vue'
// 方式1:使用InstanceType(推荐)
type ChildInstance = InstanceType<typeof Child>
const childRef = ref<ChildInstance | null>(null)
// 调用时有完整类型提示
childRef.value?.search('keyword') // ✅ IDE知道search方法
// 方式2:直接注解组件实例
const childRef = ref<InstanceType<typeof Child> | null>(null)
// 方式3:导出组件实例类型
// 子组件中
export interface ChildAPI {
search(query: string): Promise<any[]>
reset(): void
}
// 父组件中
import type { ChildAPI } from './Child.vue'
const childRef = ref<ChildAPI | null>(null)
</script>
3.7 [3]多层属性穿透($attrs)
01.多层属性穿透($attrs)
a.概述
$attrs用于多层组件嵌套时,透传属性和事件。
当组件没有在props中声明的属性会自动落入$attrs中,可以通过v-bind="$attrs"穿透到子组件。
这避免了在中间层组件重复声明和传递props。
b.完整代码示例
a.最外层父组件(GrandparentComponent.vue)
<template>
<div>
<h2>祖父组件</h2>
<!-- 传递多个属性到孙组件 -->
<MiddleComponent
:title="'表单标题'"
:placeholder="'输入内容'"
:maxlength="100"
:required="true"
/>
</div>
</template>
<script setup>
import MiddleComponent from './MiddleComponent.vue'
</script>
b.中间层组件(MiddleComponent.vue)
<template>
<div class="middle">
<h3>中间组件</h3>
<!-- ✅ 不需要声明所有props,直接穿透给FormInput -->
<FormInput v-bind="$attrs" />
</div>
</template>
<script setup>
import FormInput from './FormInput.vue'
// 这里没有定义任何props,$attrs包含所有传进来的属性
</script>
c.最终使用的子组件(FormInput.vue)
<template>
<div>
<p v-if="$attrs.title" class="title">{{ $attrs.title }}</p>
<input
type="text"
:placeholder="$attrs.placeholder"
:maxlength="$attrs.maxlength"
:required="$attrs.required"
/>
</div>
</template>
<script setup>
// 这里可以定义需要的props,其他的落在$attrs中
defineProps({
// 可以不定义任何props,全部在$attrs中
})
</script>
d.使用 $inheritAttrs 控制继承
<template>
<div class="form-wrapper">
<!-- 通过selectedAttrs选择性应用属性 -->
<label v-if="title">{{ title }}</label>
<input
v-bind="{
placeholder: $attrs.placeholder,
maxlength: $attrs.maxlength
}"
/>
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
title: String
})
// 获取除了title之外的所有$attrs
const remainingAttrs = computed(() => {
const { title, ...rest } = $attrs
return rest
})
</script>
<style scoped>
/* 禁用自动继承,手动控制 */
:deep(input) {
padding: 8px;
border: 1px solid #ccc;
}
</style>
c.说明
$attrs包含所有未在props中声明的属性
使用 v-bind="$attrs" 将所有属性穿透给子组件
适合多层嵌套组件中,中间层不需要处理的数据
$inheritAttrs: false 可禁用自动继承到根元素
可以手动选择穿透哪些属性
多层穿透避免了重复声明props
3.8 [4]跨层级通信(Provide/Inject)
00.汇总
a.核心概念速查表
概念 定义 方向 何时用 风险
Provide 祖先提供数据/方法 祖先→后代 深层嵌套 流向不透明
Inject 后代注入数据/方法 单向接收 接收数据 不知来源
响应式Provide Provide的值是reactive 双向更新 共享状态 混淆关系
default inject的默认值 fallback 防报错 难调试
Symbol inject的key类型 唯一标识 防冲突 维护复杂
b.Provide/Inject 的 JS 知识点
JS特性 在Provide中的应用 在Inject中的应用 原理
闭包 保存注入数据 访问祖先作用域 词法作用域
原型链 组件树结构 查询链路 树遍历
Symbol 作为唯一key 类型安全 象征性
WeakMap Vue内部存储 性能优化 垃圾回收
发布-订阅 provide更新 inject响应 依赖追踪
上下文 全局变量模拟 隐式依赖 魔法变量
c.一句话总结
Provide/Inject 是跨层级传值的便捷方式,但单向流向、来源不透明、易产生隐式依赖,应谨慎使用,最好定义明确的接口类型。
d.Provide/Inject 与 Slots 的选择
场景 Provide/Inject Slots 推荐
传递配置数据 ✅ ❌ Provide/Inject
定制渲染方式 ❌ ✅ Slots
深层传值 ✅ ⚠️ Provide/Inject
祖先控制子代展示 ❌ ✅ Slots
共享方法 ✅ ❌ Provide/Inject
e.快速判断表
问题:我要在两个组件间通讯
Q1: 是父子吗?
├─ YES → Props + Emit ⭐⭐⭐⭐⭐
└─ NO → Q2
Q2: 是兄弟吗?
├─ YES → 事件总线 或 Pinia ⭐⭐⭐⭐
└─ NO → Q3
Q3: 是深层嵌套吗?
├─ YES → Q4
└─ NO → 用Props逐层 ⭐⭐⭐
Q4: 是组件库吗?
├─ YES → Provide/Inject ⭐⭐⭐⭐
├─ 需要定制展示? YES → Slots ⭐⭐⭐⭐
└─ 其他 → Pinia ⭐⭐⭐⭐⭐
结论:
✅ 优先 Props+Emit
✅ 次选 Pinia(复杂项目)
⭐ Provide/Inject(组件库/框架级)
⚠️ 要明确类型定义和接口
f.何时使用 Provide/Inject
a.适合用
a.第第三方组件库(如Element Plus)
组件库需要配置全局样式主题
使用provide/inject传递配置
b.应应用主题切换
祖先provide主题
所有后代自动响应主题变化
c.大大型表格/树形结构内部通讯
行组件与表格通讯
树节点与树根通讯
d.权权限/认证上下文
提供用户信息和权限
b.不适合用
1.简简单的父子通讯 → 用Props+Emit
2.兄兄弟组件通讯 → 用事件总线或Pinia
3.全全局应用状态 → 用Pinia/Vuex
4.频频繁变化的数据 → 用Pinia(性能更好)
01.跨层级通信(Provide/Inject)
a.概述
Provide/Inject用于祖先组件向深层后代组件传递数据。
祖先组件通过provide提供数据,任意层级的后代组件都可以通过inject获取这些数据。
这避免了多层组件逐级传递props的冗长代码,特别适合深层嵌套的组件树。
b.完整代码示例
a.祖先组件(GrandparentComponent.vue)
<template>
<div class="grandparent">
<h2>祖先组件</h2>
<p>主题: {{ theme }}</p>
<button @click="toggleTheme">切换主题</button>
<ParentComponent />
</div>
</template>
<script setup>
import { ref, provide, computed } from 'vue'
import ParentComponent from './ParentComponent.vue'
const theme = ref('light')
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
// ✅ 提供数据给所有后代组件
provide('theme', theme)
// 提供包含方法的对象
provide('themeContext', {
theme,
toggleTheme,
isLight: computed(() => theme.value === 'light'),
isDark: computed(() => theme.value === 'dark')
})
</script>
b.中间组件(ParentComponent.vue)
<template>
<div class="parent">
<h3>父组件(中间层)</h3>
<!-- 中间层不需要处理数据,直接传递 -->
<ChildComponent />
</div>
</template>
<script setup>
import ChildComponent from './ChildComponent.vue'
</script>
c.后代组件(ChildComponent.vue)
<template>
<div class="child" :class="theme">
<h4>后代组件</h4>
<p>当前主题: {{ theme }}</p>
<p v-if="isDark">深色模式</p>
<p v-else>浅色模式</p>
<button @click="toggleTheme">切换主题</button>
</div>
</template>
<script setup>
import { inject } from 'vue'
// ✅ 注入祖先组件提供的数据
const themeContext = inject('themeContext')
const { theme, toggleTheme, isDark } = themeContext
</script>
<style scoped>
.child.light {
background: white;
color: black;
}
.child.dark {
background: black;
color: white;
}
</style>
c.说明
祖先组件使用 provide(key, value) 提供数据
后代组件使用 inject(key) 获取数据
跳过中间层,直接传递数据给深层后代
provide可以提供响应式数据(ref/reactive)
inject可以有默认值:inject('key', defaultValue)
仅适合祖先→后代方向,反向无法通讯
02.Provide/Inject 基础用法
a.方式1:基础值传递
---
// 祖先组件
<script setup>
import { provide, ref } from 'vue'
const theme = ref('dark')
// 提供一个值
provide('theme', theme)
</script>
// 孙组件
<script setup>
import { inject } from 'vue'
// 接收值
const theme = inject('theme')
console.log(theme.value) // 'dark',注意要用.value
</script>
---
b.方式2:提供方法
---
// 祖先组件
<script setup>
import { provide } from 'vue'
const toggleTheme = () => {
// 切换主题逻辑
}
provide('toggleTheme', toggleTheme)
</script>
// 后代组件
<script setup>
import { inject } from 'vue'
const toggleTheme = inject('toggleTheme')
toggleTheme() // 调用祖先的方法
</script>
---
c.方式3:提供对象(常见)
---
// 祖先组件
<script setup>
import { provide, ref, computed } from 'vue'
const user = ref({ name: 'John', role: 'admin' })
const permissions = ['read', 'write', 'delete']
provide('userContext', {
user,
permissions,
isAdmin: computed(() => user.value.role === 'admin')
})
</script>
// 后代组件(任何层级)
<script setup>
import { inject } from 'vue'
const userContext = inject('userContext')
console.log(userContext.user.value.name) // 'John'
console.log(userContext.permissions) // ['read', 'write', 'delete']
console.log(userContext.isAdmin.value) // true
</script>
---
03.Provide/Inject 的核心限制
a.限制1:单向流向(不能反向)
---
// 结构:
// Ancestor
// ├─ Child
// └─ Grandchild
// 祖先提供数据给后代
provide('data', ref(0))
// ✅ 后代可以接收
inject('data') // 可以
// ❌ 但后代要改变祖先的数据呢?
// 无法直接通过inject改!
// ✅ 解决方案:提供方法
provide('updateData', (newVal) => {
data.value = newVal
})
// 后代调用提供的方法来改变祖先的数据
const updateData = inject('updateData')
updateData(100)
---
结论:Provide/Inject 是严格的单向流,后代无法直接改变祖先状态,只能通过"回调方法"来通知
b.限制2:无法跨sibling(兄弟无法通讯)
---
// 结构:
// Parent
// ├─ Child A
// └─ Child B
// Parent中provide的数据
provide('shared', ref(0))
// ✅ Child A 和 Child B 都能接收
// 但他们能相互通讯吗?
// ❌ 无法!需要通过Parent的emit来中转
---
c.限制3:来源不透明
---
<script setup>
// 在这个组件中使用inject,你知道数据来自哪个祖先吗?
const theme = inject('theme') // ❌ 不知道来自谁
// 如果多个祖先都provide('theme'),用哪个?
// Vue会用最近的祖先提供的值
</script>
---
04.Provide/Inject 对比其他方案
a.vs Props(多层传递)
Props逐层传递:
Parent → Child (props) → Grandchild (props)
Provide/Inject:
Parent (provide) → ... → Grandchild (inject)
对比:
Props:
✅ 清晰可追踪
❌ 多层嵌套时冗长
用途:少于3层用Props
Provide/Inject:
✅ 跳过中间层
❌ 来源不透明
用途:多于3层用Provide/Inject
b.vs 作用域插槽(Slots)
---
// Provide/Inject 方式
<script setup>
provide('theme', 'dark')
</script>
<template>
<Child />
</template>
// 子组件无法控制展示
// 作用域插槽方式
<template>
<slot :theme="'dark'" />
</template>
// 父组件可以完全控制展示方式
---
选择:
a.需要传数据给后代显示 → Provide/Inject
b.需要后代定制展示方式 → Slots
c.vs Pinia(全局状态)
Provide/Inject:
✅ 轻量级,无需额外库
❌ 限制在组件树内
❌ 难以追踪,难以测试
Pinia:
✅ 完整的状态管理
✅ 跨越任何组件
❌ 学习成本高
选择:
a.小功能区域共享状态 → Provide/Inject
b.全局应用状态 → Pinia
05.Provide/Inject 的安全隐患
a.隐患1:数据流向不透明(最大问题)
---
// 组件中inject一个值
const userData = inject('userData')
// 问题:我怎么知道这个数据来自哪里?
// - 来自Parent?
// - 来自Ancestor?
// - 还是根组件?
// IDE无法追踪,只能手动搜索grep
// 导致的后果:
// - 难以调试
// - 难以重构
// - 难以测试(必须mock provide)
---
b.隐患2:隐式依赖
---
// 这个组件依赖于什么?
<script setup>
const theme = inject('theme')
const user = inject('user')
const permissions = inject('permissions')
</script>
// 从代码看不出来有什么依赖!
// 必须import组件后运行才知道(runtime错误)
// vs Props的显式依赖
<script setup>
defineProps({
theme: String,
user: Object,
permissions: Array
})
</script>
// 一眼看出依赖什么
---
c.隐患3:名称冲突
---
// Ancestor 1
provide('config', ref({ theme: 'dark' }))
// Ancestor 2
provide('config', ref({ lang: 'en' }))
// Descendant
const config = inject('config')
// 哪个config会被注入?最近的祖先!
// 但如果组件在不同树位置时结果不同
// 造成隐蔽的bug
---
d.隐患4:直接修改状态(失控)
---
// Parent provide了响应式对象
provide('state', reactive({
count: 0,
user: { name: 'John' }
}))
// Descendant直接修改
const state = inject('state')
state.count = 100 // ⚠️ 直接修改
state.user.name = 'Jane' // ⚠️ 深层修改
// Parent无法拦截这些修改!
// 无法进行验证、权限检查等
---
e.隐患5:循环注入
---
// Parent provide了injector引用
provide('injector', inject)
// Descendant可以inject任何东西
const injector = inject('injector')
const anyValue = injector('anyKey')
// 完全失控!可以造成安全漏洞
---
06.安全的 Provide/Inject 最佳实践
a.方案1:明确的接口定义
---
// context/userContext.ts
import { ref, computed } from 'vue'
import type { InjectionKey } from 'vue'
export interface UserContext {
user: Ref<{ name: string; role: string }>
isAdmin: ComputedRef<boolean>
updateUser: (name: string) => void
}
// 用Symbol作为key,防止冲突
export const UserContextKey: InjectionKey<UserContext> = Symbol('UserContext')
// Ancestor
<script setup>
import { provide, ref, computed } from 'vue'
import { UserContextKey } from './context/userContext'
const user = ref({ name: 'John', role: 'admin' })
provide(UserContextKey, {
user,
isAdmin: computed(() => user.value.role === 'admin'),
updateUser: (name) => { user.value.name = name }
})
</script>
// Descendant
<script setup>
import { inject } from 'vue'
import { UserContextKey } from './context/userContext'
const userContext = inject(UserContextKey)!
// TypeScript能完全推断类型!
userContext.updateUser('Jane')
console.log(userContext.isAdmin.value)
</script>
---
优点:
a.类型安全
b.IDE有自动完成
c.清晰的接口契约
b.方案2:只provide方法,不provide状态
---
// 父组件只提供行为
provide('actions', {
async fetchUser() { /* ... */ },
async updateUser(data) { /* ... */ },
logout() { /* ... */ }
})
// 不提供state!让组件自己管理
// 后代组件
const { fetchUser, updateUser, logout } = inject('actions')
const user = ref(null)
onMounted(() => {
fetchUser().then(data => user.value = data)
})
---
优点:
a.后代有明确的职责
b.状态清晰可控
c.易于测试
c.方案3:限制provide范围
---
// ❌ 在根组件provide(作用范围太广)
// main.js
app.provide('config', globalConfig)
// ✅ 在特定组件树provide
// PageContainer.vue
<script setup>
import { provide } from 'vue'
provide('pageContext', {
currentPage: ref('list'),
filters: ref({})
})
</script>
<template>
<div class="page">
<Filters />
<List />
<Pagination />
</div>
</template>
// 只在PageContainer的子树中可用,外部不受影响
---
3.9 [5]作用域插槽(Slots)
00.汇总
a.核心概念速查表
概念 定义 JS原理 何时用
作用域插槽 子组件暴露数据/方法,父组件消费 JS闭包、参数传递 父定内容,子供数据
slot props 插槽提供的数据对象 参数解构、作用域链 父组件拿到子数据
具名插槽 多个插槽+自定义名字 对象属性解构、动态绑定 定制性更强
默认插槽 没名字的slot 模板占位 单一内容插入
函数式插槽 模板即函数,props为参数 函数式编程 灵活内容自定义
b.JS知识点在作用域插槽中的应用
JS特性 在作用域插槽中的体现 作用
闭包 插槽props数据暴露 父模板能“消费”子数据
参数解构 v-slot/#default用法 结构化访问props数据
作用域链 插槽props和父变量互不冲突 隔离作用域、避免变量污染
函数传值 slot传递方法、回调 灵活交互、自定义行为
响应式原理 插槽props响应式 父组件可直接用子数据渲染
上下文 全局变量模拟 隐式依赖 魔法变量
c.一句话总结
作用域插槽让子组件暴露数据、父组件自定义内容,原理依赖闭包和参数传递,是Vue3中最高级也最灵活的父子通讯与UI定制方式。
d.插槽 VS 其他通讯方式
方式 本质 灵活性 推荐场景
作用域插槽 子供props、父定内容 4星 UI定制/高复用
普通插槽 父定内容结构 2星 简单内容占位
Provide/Inject 祖先→后代数据流 2星 深度配置共享
Props+Emit 父传子/子传父 3星 基础状态通信
e.常见误区与实践指南
插槽props不可直接修改(单向数据流,回调事件需emit/props通知)
插槽内容总是父组件作用域(不可访问子组件本地变量,除props暴露外)
解构slot-props能防止变量名冲突
嵌套多级插槽建议具名slot,防止错误插入
保持slot props结构简单清晰
f.作用域插槽常用场景
动态表格渲染(el-table, ant-table)
列表、下拉框、级联器等自定义选项
Form表单组件自定义输入区
弹窗、Dialog、Popover自定义内容区
UI组件库高度定制内容
g.总结口诀
“子供数据,父定内容”,灵活自定义UI场景作用域插槽必学。
01.作用域插槽 (Scoped Slots)
a.概述
作用域插槽是一种高级插槽机制,它允许子组件在渲染其插槽时,将自身的数据传递给父组件的插槽内容。
这使得父组件可以决定如何渲染来自子组件的数据。
它就像一个“带参数的插槽”,极大地增强了组件的灵活性和复用性,常用于创建可定制的列表、表格等布局组件。
b.完整代码示例
a.子组件(`UserListComponent.vue`)
这个组件负责管理用户列表数据,但通过作用域插槽把每个用户的渲染权交给了父组件。
<template>
<div class="user-list">
<h4>用户列表(子组件)</h4>
<ul>
<li v-for="(user, index) in users" :key="user.id">
<!--
这里是关键:通过 v-bind 将 user 对象和 index 传给插槽,
父组件在 v-slot 中就可以接收到它们。
-->
<slot :user="user" :index="index"></slot>
</li>
</ul>
</div>
</template>
<script setup>
import { ref } from 'vue'
// 子组件内部维护的数据
const users = ref([
{ id: 1, name: 'Alice', role: 'Admin' },
{ id: 2, name: 'Bob', role: 'User' },
{ id: 3, name: 'Charlie', role: 'Guest' }
])
</script>
<style scoped>
.user-list {
border: 1px solid #ccc;
padding: 10px;
margin-top: 20px;
}
ul {
list-style-type: none;
padding: 0;
}
li {
background-color: #f9f9f9;
margin-bottom: 5px;
padding: 8px;
}
</style>
b.父组件(`ParentComponent.vue`)
父组件使用 `UserListComponent`,并通过 `v-slot` 定义了每个列表项的具体渲染方式。
<template>
<div>
<h2>父组件</h2>
<p>下面是使用作用域插槽自定义渲染的用户列表:</p>
<user-list-component>
<!--
使用 <template v-slot:default="{ user, index }"> 来接收数据。
#default 是 v-slot:default 的简写。
{ user, index } 是对插槽 props 对象的解构。
-->
<template #default="{ user, index }">
<div class="custom-item">
<strong>{{ index + 1 }}. {{ user.name }}</strong>
<em :class="getRoleClass(user.role)"> ({{ user.role }}) </em>
</div>
</template>
</user-list-component>
</div>
</template>
<script setup>
import UserListComponent from './UserListComponent.vue'
const getRoleClass = (role) => {
return role === 'Admin' ? 'role-admin' : 'role-user';
}
</script>
<style scoped>
.custom-item strong {
color: #3498db;
}
.custom-item .role-admin {
color: #e74c3c;
font-weight: bold;
}
.custom-item .role-user {
color: #2ecc71;
}
</style>
c. 说明
子组件:通过在 `<slot>` 标签上使用 `v-bind` (例如 `:user="user"`) 来暴露数据给父组件。
父组件:在子组件标签内部使用 `<template>` 标签,并通过 `v-slot` (简写为 `#`) 指令来接收插槽传递的数据。
数据接收:`v-slot` 的值是一个包含了所有从子组件传来属性的对象(通常称为 `slotProps`),推荐使用ES6解构赋值直接获取数据,如 `v-slot="{ user, index }"`,代码更简洁。
核心价值:该模式将**“什么数据被渲染”**(由子组件决定)与**“数据如何被渲染”**(由父组件决定)彻底分离,实现了高度的组件可定制性。
02.代码实战
a.作用域插槽基础模式与代码示例
a.子组件暴露插槽数据
---
<!-- ChildTable.vue -->
<template>
<tbody>
<tr v-for="(user, i) in users" :key="user.id">
<slot :user="user" :index="i" />
</tr>
</tbody>
</template>
<script setup>
import { ref } from 'vue'
const users = ref([
{ id: 1, name: 'Tom', role: 'admin' },
{ id: 2, name: 'Lucy', role: 'user' }
])
</script>
---
b.父组件消费插槽props(推荐解构写法)
---
<!-- Parent.vue -->
<ChildTable v-slot="{ user, index }">
<td>{{ index + 1 }}</td>
<td>{{ user.name }}</td>
<td>{{ user.role }}</td>
</ChildTable>
<!-- 也可以写成具名插槽(适合多个插槽) -->
<ChildTable #default="{ user, index }">
<td>{{ index + 1 }}</td>
<td>{{ user.name }}</td>
<td>{{ user.role }}</td>
</ChildTable>
---
b.进阶用法与技巧
a.具名插槽 + 作用域插槽结合
---
<!-- 子组件 -->
<slot name="footer" :actions="actionList" />
<!-- 父组件 -->
<template #footer="{ actions }">
<button v-for="act in actions" :key="act.type">{{ act.label }}</button>
</template>
---
b.插槽传递响应式方法
---
<!-- 子组件 -->
<slot :onEdit="editItem" :item="item" />
<!-- 父组件 -->
<template #default="{ onEdit, item }">
<button @click="onEdit(item)">编辑</button>
</template>
---
3.10 [6]兄弟组件通信(事件总线mitt)
00.汇总
a.核心概念速查表
方式 通讯对象 原理 学习成本 推荐度 何时用
事件总线(mitt) 任意兄弟 发布-订阅 ⭐⭐⭐ 中 ⭐⭐⭐⭐ 兄弟通讯
$attrs 多层穿透 属性穿透 ⭐⭐ 中低 ⭐⭐⭐⭐ 多层嵌套
Pinia 全局共享 状态管理 ⭐⭐⭐⭐ 高 ⭐⭐⭐⭐⭐ 大型应用
Slots 父←→子 函数作用域 ⭐⭐ 中低 ⭐⭐⭐⭐ 内容分发
$parent 直接父级 实例引用 ⭐ 低 ⭐ 紧急
$children 所有子级 实例数组 ⭐ 低 ⭐ 紧急
b.兄弟 & 全局通讯的 JS 知识点
JS特性 应用场景 原理
事件系统 mitt的emit/on 发布-订阅模式
Map/Set mitt的数据结构 高效查询
WeakMap mitt的生命周期 垃圾回收优化
闭包 事件处理函数 保存状态
原型链 $parent/$children 组件树遍历
Object.assign $attrs合并 对象属性扩展
Reactive/Proxy Pinia响应式 双向绑定
c.一句话总结
兄弟通讯用mitt(轻)或Pinia(重),多层穿透用$attrs或Provide/Inject,全局共享必用Pinia,避免使用$parent/$children。
d.$parent 与 $children(不推荐)
// ❌ 问题1:强耦合
// 子组件直接访问父组件实例
this.$parent.data = 100 // 改变父组件数据
this.$parent.method() // 调用父组件方法
// 后果:子组件严重依赖特定的父组件结构
// 如果父组件结构改变,子组件就破裂
// ❌ 问题2:不兼容Composition API
// $parent 在Composition API中不可用
// getCurrentInstance()?.parent 存在但不推荐
// ❌ 问题3:难以维护
// 数据流不透明,无法追踪
// IDE无法智能补全
e.场景选择速记
---
Q1: 是不相关的兄弟组件吗?
├─ 仅2-3个 → mitt ✅
├─ 多个+复杂交互 → Pinia ✅✅
└─ 否 → Q2
Q2: 是多层嵌套吗?
├─ 3层+ → Provide/Inject ✅
├─ 2层 → Props逐层 ✅
└─ 否 → Q3
Q3: 需要自定义展示吗?
├─ YES → Slots ✅✅
└─ 否 → Props+Emit ✅✅✅
Q4: 全局应用状态?
├─ YES → Pinia ✅✅✅
└─ 否 → 上述方案
最终建议:
✅ 默认用:Props+Emit
✅ 复杂用:Pinia
⭐ 特定用:Provide/Inject, Slots, mitt
❌ 避免用:$parent/$children
01.兄弟组件通信(事件总线 mitt)
a.概述
事件总线使用发布-订阅模式实现兄弟组件间的通信。
通过一个中央事件管理器(如mitt),组件可以发送和接收事件。
这避免了通过父组件中转的复杂代码,特别适合兄弟组件或无直接关系的组件通信。
b.完整代码示例
a.创建事件总线(utils/eventBus.js)
import mitt from 'mitt'
// 创建事件总线实例
export const eventBus = mitt()
// 定义事件类型(可选,用于TypeScript)
export const EventTypes = {
USER_LOGIN: 'user-login',
USER_LOGOUT: 'user-logout',
THEME_CHANGE: 'theme-change',
DATA_SYNC: 'data-sync'
}
b.兄弟组件A - 发送事件(ComponentA.vue)
<template>
<div class="component-a">
<h3>组件A(发送者)</h3>
<input v-model="message" placeholder="输入消息" />
<button @click="sendMessage">发送给兄弟组件</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { eventBus, EventTypes } from '@/utils/eventBus'
const message = ref('')
const sendMessage = () => {
// ✅ 发送事件
eventBus.emit(EventTypes.DATA_SYNC, {
from: 'ComponentA',
message: message.value,
timestamp: new Date()
})
message.value = ''
}
</script>
c.兄弟组件B - 接收事件(ComponentB.vue)
<template>
<div class="component-b">
<h3>组件B(接收者)</h3>
<p v-if="receivedData">
收到消息: {{ receivedData.message }}
</p>
<p v-else>等待消息...</p>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { eventBus, EventTypes } from '@/utils/eventBus'
const receivedData = ref(null)
// 事件处理函数
const handleDataSync = (data) => {
console.log('收到数据:', data)
receivedData.value = data
}
onMounted(() => {
// ✅ 监听事件
eventBus.on(EventTypes.DATA_SYNC, handleDataSync)
})
// ✅ 重要:在组件卸载时清理事件监听,防止内存泄漏
onUnmounted(() => {
eventBus.off(EventTypes.DATA_SYNC, handleDataSync)
})
</script>
d.父组件(ParentComponent.vue)
<template>
<div>
<h2>父组件</h2>
<ComponentA />
<ComponentB />
</div>
</template>
<script setup>
import ComponentA from './ComponentA.vue'
import ComponentB from './ComponentB.vue'
</script>
c.说明
mitt是一个轻量级的事件总线库
eventBus.emit('eventName', data) 发送事件
eventBus.on('eventName', handler) 监听事件
必须在onUnmounted中调用off清理,防止内存泄漏
适合兄弟组件通信,避免通过父组件层层转发
建议使用常量定义事件名,避免硬编码字符串
事件名建议使用 kebab-case(短横线)
02.代码示例
a.事件总线 (mitt)
a.安装与基础设置
---
// 1. 安装
// npm install mitt
// 2. 创建事件总线(推荐:封装到单独文件)
// utils/eventBus.ts
import mitt from 'mitt'
type Events = {
'user-login': { id: number; name: string }
'user-logout': null
'theme-change': 'light' | 'dark'
'data-update': any
}
export const eventBus = mitt<Events>()
// 3. 在main.js中(可选:全局注册)
// import { eventBus } from './utils/eventBus'
// app.config.globalProperties.$bus = eventBus
---
b.完整使用示例
---
// ComponentA.vue - 发送事件
<template>
<button @click="login">登录</button>
</template>
<script setup>
import { eventBus } from '@/utils/eventBus'
const login = () => {
// 发送事件
eventBus.emit('user-login', { id: 1, name: 'John' })
}
</script>
// ComponentB.vue - 接收事件
<script setup>
import { onMounted, onUnmounted } from 'vue'
import { eventBus } from '@/utils/eventBus'
const handleLogin = (data) => {
console.log('用户登录:', data)
// 响应登录事件
}
onMounted(() => {
// 监听事件
eventBus.on('user-login', handleLogin)
})
// ✅ 重要:及时清理,防止内存泄漏
onUnmounted(() => {
eventBus.off('user-login', handleLogin)
})
</script>
---
b.常见错误与解决
a.错误1:内存泄漏(未清理事件)
---
// ❌ 错误
onMounted(() => {
eventBus.on('login', handler) // 未在onUnmounted中清理
})
// 后果:组件卸载后事件监听仍存在
// 每次重新mount都会添加新的监听
// 最终同一个事件会触发多次
// ✅ 正确
onMounted(() => {
eventBus.on('login', handler)
})
onUnmounted(() => {
eventBus.off('login', handler) // 必须清理
})
---
b.错误2:事件泛滥
---
// ❌ 问题:事件总线用于所有通讯
eventBus.emit('data-update')
eventBus.emit('state-change')
eventBus.emit('any-event')
// 最后变成了全局大杂烩,难以维护
// ✅ 规范
// 1. 仅用于不相关组件的通讯
// 2. 兄弟通讯尽量用父组件转发
// 3. 复杂业务用Pinia
---
c.错误3:多层嵌套事件混乱
---
// ✅ 最佳实践:集中管理事件名
// constants/events.ts
export const EventNames = {
USER_LOGIN: 'user-login',
USER_LOGOUT: 'user-logout',
THEME_CHANGE: 'theme-change',
FORM_SUBMIT: 'form-submit'
} as const
// 组件中
import { EventNames } from '@/constants/events'
eventBus.emit(EventNames.USER_LOGIN, data)
eventBus.on(EventNames.USER_LOGIN, handler)
---
3.11 [7]全局状态管理(Pinia)
01.全局状态管理(Pinia)
a.概述
Pinia是Vue3推荐的状态管理库,用于管理全局应用状态。
多个组件可以共享和修改同一个全局状态。
Pinia提供了完整的响应式系统、时间旅行调试等功能,特别适合大型应用中复杂的跨组件数据同步需求。
b.完整代码示例
a.创建Store(stores/userStore.js)
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
// ✅ 定义状态
const user = ref(null)
const isLoggedIn = ref(false)
const permissions = ref([])
// ✅ 定义派生状态(计算属性)
const isAdmin = computed(() => {
return user.value?.role === 'admin'
})
const userName = computed(() => {
return user.value?.name || 'Guest'
})
// ✅ 定义修改状态的方法
const login = async (credentials) => {
try {
// 模拟API调用
const response = await new Promise(resolve => {
setTimeout(() => {
resolve({
user: { id: 1, name: 'John', role: 'admin' },
permissions: ['read', 'write', 'delete']
})
}, 1000)
})
user.value = response.user
permissions.value = response.permissions
isLoggedIn.value = true
return { success: true }
} catch (error) {
return { success: false, error }
}
}
const logout = () => {
user.value = null
permissions.value = []
isLoggedIn.value = false
}
const updateUserInfo = (newInfo) => {
if (user.value) {
user.value = { ...user.value, ...newInfo }
}
}
// ✅ 暴露状态和方法
return {
// state
user,
isLoggedIn,
permissions,
// computed
isAdmin,
userName,
// methods
login,
logout,
updateUserInfo
}
})
b.组件A - 修改状态(ComponentA.vue)
<template>
<div class="component-a">
<h3>组件A(修改状态)</h3>
<button v-if="!userStore.isLoggedIn" @click="handleLogin">
登录
</button>
<button v-else @click="handleLogout">
退出登录
</button>
</div>
</template>
<script setup>
import { useUserStore } from '@/stores/userStore'
const userStore = useUserStore()
const handleLogin = async () => {
// ✅ 调用store的login方法
const result = await userStore.login({
username: 'john',
password: '123456'
})
if (result.success) {
console.log('登录成功')
}
}
const handleLogout = () => {
// ✅ 调用store的logout方法
userStore.logout()
}
</script>
c.组件B - 读取状态(ComponentB.vue)
<template>
<div class="component-b">
<h3>组件B(读取状态)</h3>
<div v-if="userStore.isLoggedIn">
<p>欢迎: {{ userName }}</p>
<p v-if="isAdmin">管理员权限</p>
<p>权限: {{ userStore.permissions.join(', ') }}</p>
</div>
<div v-else>
<p>请先登录</p>
</div>
</div>
</template>
<script setup>
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/userStore'
const userStore = useUserStore()
// ✅ 使用storeToRefs解构响应式数据
const { userName, isAdmin } = storeToRefs(userStore)
// 当组件A修改状态时,组件B自动更新
</script>
d.在main.js中配置Pinia
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
// ✅ 安装Pinia
app.use(createPinia())
app.mount('#app')
c.说明
Pinia是Vue3官方推荐的状态管理库
使用 defineStore('storeName', () => { ... }) 定义store
状态通过ref/reactive定义,自动响应式
组件通过 useXxxStore() 引入store
使用 storeToRefs 解构store中的响应式数据
当一个组件修改状态时,其他组件自动同步更新
适合大型应用中的全局状态管理,避免事件总线的复杂性
3.12 [8]访问父/子实例(parent/children)
01.访问父/子实例 ($parent / $children)
a.概述
`$parent` 和 `$children` 是 Vue 实例上的属性,允许一个组件直接访问其父组件实例或子组件实例数组。
这是一种侵入式的组件通信方式,因为它打破了“单向数据流”(Props 向下传递,Events 向上触发)的原则,造成了组件间的**紧密耦合**。
在现代 Vue 开发中,**强烈不推荐**使用 `$parent` 和 `$children`。
它们会使应用变得脆弱且难以维护。对于跨层级通信,首选的替代方案是 `Provide / Inject`。
b.完整代码示例 (基于 Vue 2 / Options API 风格)
a.父组件(`ParentComponent.vue`)
<template>
<div class="parent">
<h2>父组件</h2>
<p>父组件的数据: {{ name }}</p>
<button @click="accessChildren">访问所有子组件</button>
<child-component ref="child1" />
<child-component ref="child2" />
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue'
export default {
name: 'ParentComponent',
components: { ChildComponent },
data() {
return {
name: 'Parent'
}
},
methods: {
parentMethod(childName) {
console.log(`父组件的方法被 '${childName}' 调用了!`);
},
accessChildren() {
this.$children.forEach((child, index) => {
// 调用每个子组件上的方法
child.childMethod(index + 1);
});
}
}
}
</script>
<style scoped>
.parent {
border: 2px solid #41b883;
padding: 15px;
}
</style>
b.子组件(`ChildComponent.vue`)
<template>
<div class="child">
<h3>子组件</h3>
<!-- 直接访问父组件的数据 -->
<p>从父组件读取的数据: {{ $parent.name }}</p>
<button @click="callParent">调用父组件方法</button>
</div>
</template>
<script>
export default {
name: 'ChildComponent',
data() {
return {
name: 'Child'
}
},
methods: {
callParent() {
// 直接调用父组件的方法
this.$parent.parentMethod(this.name);
},
childMethod(index) {
console.log(`子组件 #${index} 的方法被父组件调用了。`);
}
}
}
</script>
<style scoped>
.child {
border: 1px dashed #34495e;
padding: 10px;
margin-top: 10px;
}
</style>
c.说明
`$parent`**:子组件通过 `this.$parent` 可以直接访问其父组件的整个实例,包括 `data`、`computed`、`methods` 等。
`$children`**:父组件通过 `this.$children` 可以访问一个包含其所有**直接子组件**实例的数组。**注意**:这个数组的顺序不被保证,并且它不是响应式的。
d.严重缺点 (为什么不要用)
1.紧密耦合:子组件依赖于父组件的内部实现。如果父组件的方法或属性名改变,子组件会立刻出错。
2.脆弱性:如果组件结构发生变化(例如,在父子之间增加一个包装组件),`$parent` 的指向就会错误,导致应用崩溃。
e.Vue 3 (`<script setup>`) 中的情况
在 `<script setup>` 中,没有 `this`,因此不能直接使用 `this.$parent`。
需要通过 `getCurrentInstance` API 来变相获取,例如 `getCurrentInstance().parent`。但这属于内部 API,官方文档明确指出**不应在应用代码中使用它**。
结论:在组合式 API 中,这个模式被彻底废弃,请使用 `provide/inject` 作为替代。
4 mixins
4.1 汇总:7种
01.对比1
【第一梯队:Vue3官方推荐】
┌─────────────────────────┬──────────────────────┬─────────┐
│ 组合式函数(Composables) │ 所有组件间逻辑复用 │ ⭐⭐⭐⭐⭐ │
├─────────────────────────┼──────────────────────┼─────────┤
│ 嵌套 Composables │ 复杂逻辑高度模块化 │ ⭐⭐⭐⭐ │
└─────────────────────────┴──────────────────────┴─────────┘
【第二梯队:特定场景必用】
┌─────────────────────────┬──────────────────────┬─────────┐
│ 插件系统 (app.use()) │ 第三方库集成/应用扩展 │ ⭐⭐⭐ │
├─────────────────────────┼──────────────────────┼─────────┤
│ 自定义指令 (directive) │ DOM操作/行为控制 │ ⭐⭐⭐ │
├─────────────────────────┼──────────────────────┼─────────┤
│ Pinia/Vuex+Composables │ 全局状态集中管理 │ ⭐⭐⭐⭐ │
└─────────────────────────┴──────────────────────┴─────────┘
【第三梯队:Vue2遗留/兼容,不推荐】
┌─────────────────────────┬──────────────────────┬─────────┐
│ Options API 的局部 mixin │ 快速复用(有隐患) │ ⭐⭐ │
├─────────────────────────┼──────────────────────┼─────────┤
│ 全局 mixin │ 全局污染(坚决不用) │ ⭐ │
└─────────────────────────┴──────────────────────┴─────────┘
02.对比2
方案 | 适用场景 | 优势 | 局限 | 推荐度
--------------------------|----------------------------|----------------------------|----------------------------|--------
组合式函数(Composables) | 现代项目/大多数复用场景 | 高内聚、类型支持、易测试 | 需理解响应式基础 | ⭐⭐⭐⭐⭐
嵌套 Composables | 高度模块化/复杂逻辑 | 灵活组合,逻辑清晰 | 设计不当易碎片化 | ⭐⭐⭐⭐
插件系统 (app.use()) | 第三方库集成/应用扩展 | 解耦组件,跨项目复用 | 学习成本,排查难度 | ⭐⭐⭐
自定义指令 (directive) | DOM操作/行为控制 | 解耦组件,跨项目复用 | 学习成本,排查难度 | ⭐⭐⭐
Pinia/Vuex+Composables | 大型项目/全局状态管理 | 模块化强,状态集中管理 | 小项目架构过重 | ⭐⭐⭐⭐
Options API 的局部 mixin | 快速复用/老项目兼容 | 简单直接,学习成本低 | 命名冲突,类型推导差 | ⭐⭐
全局 mixins | 全局行为统一注入 | 覆盖面广,配置集中 | 全局污染,副作用不透明 | ⭐
4.2 [1]组合式函数(Composables)
01.组合式函数(Composables)
a.定义与用法
封装函数,在 setup() 或 <script setup> 中直接调用,返回响应式数据和方法。推荐用于 Vue3 项目的所有新复用逻辑场景。
优点: 逻辑高内聚、参数灵活、类型友好,可组合性极强,易拆解和测试
不足: 初用者上手需理解响应式基础
b.Composable 定义部分
a.composables/useCounter.js
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
// 响应式状态
const count = ref(initialValue)
const doubleCount = computed(() => count.value * 2)
// 方法
function increment() {
count.value++
}
function decrement() {
count.value--
}
function reset() {
count.value = initialValue
}
// 返回需要暴露的状态和方法
return {
count,
doubleCount,
increment,
decrement,
reset
}
}
b.composables/useFetch.js
import { ref, watchEffect, toValue } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(false)
const fetchData = async () => {
loading.value = true
data.value = null
error.value = null
try {
const response = await fetch(toValue(url))
data.value = await response.json()
} catch (err) {
error.value = err
} finally {
loading.value = false
}
}
watchEffect(() => {
fetchData()
})
return { data, error, loading }
}
c.Composable 使用部分
a.CounterComponent.vue
<template>
<div class="counter">
<p>计数: {{ count }}</p>
<p>双倍: {{ doubleCount }}</p>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
<button @click="reset">重置</button>
</div>
</template>
<script setup>
import { useCounter } from '@/composables/useCounter'
// 直接调用组合函数,解构获取状态和方法
const { count, doubleCount, increment, decrement, reset } = useCounter(10)
</script>
b.UserList.vue
<template>
<div>
<div v-if="loading">加载中...</div>
<div v-else-if="error">错误: {{ error.message }}</div>
<div v-else-if="data">
<ul>
<li v-for="user in data" :key="user.id">
{{ user.name }}
</li>
</ul>
</div>
</div>
</template>
<script setup>
import { useFetch } from '@/composables/useFetch'
const { data, error, loading } = useFetch('https://api.example.com/users')
</script>
4.3 [2]嵌套 Composables
01.嵌套 Composables
a.定义与用法
多个 Composables 可相互组合嵌套,复用逻辑层层抽象。适用于高度模块化或微前端场景。
优点: 灵活组合、完全避开命名冲突,逻辑更清晰,易按需拆装
不足: 设计不佳可能形成过于碎片化的"hooks hell"
b.嵌套 Composable 定义部分
a.composables/useEventListener.js
import { onMounted, onUnmounted } from 'vue'
export function useEventListener(target, event, callback) {
onMounted(() => {
target.addEventListener(event, callback)
})
onUnmounted(() => {
target.removeEventListener(event, callback)
})
}
b.composables/useMouse.js
import { ref } from 'vue'
import { useEventListener } from './useEventListener'
export function useMouse() {
const x = ref(0)
const y = ref(0)
// 组合使用另一个composable
useEventListener(window, 'mousemove', (event) => {
x.value = event.pageX
y.value = event.pageY
})
return { x, y }
}
c.composables/useMouseInElement.js
import { ref, computed } from 'vue'
import { useMouse } from './useMouse'
export function useMouseInElement(elementRef) {
// 复用 useMouse
const { x: mouseX, y: mouseY } = useMouse()
const isInside = computed(() => {
if (!elementRef.value) return false
const rect = elementRef.value.getBoundingClientRect()
return (
mouseX.value >= rect.left &&
mouseX.value <= rect.right &&
mouseY.value >= rect.top &&
mouseY.value <= rect.bottom
)
})
return { isInside, mouseX, mouseY }
}
c.嵌套 Composable 使用部分
a.InteractiveBox.vue
<template>
<div>
<div
ref="boxRef"
class="interactive-box"
:class="{ 'is-hover': isInside }"
>
<p>鼠标位置: ({{ mouseX }}, {{ mouseY }})</p>
<p>鼠标在区域内: {{ isInside ? '是' : '否' }}</p>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useMouseInElement } from '@/composables/useMouseInElement'
const boxRef = ref(null)
// 使用嵌套组合的composable
const { isInside, mouseX, mouseY } = useMouseInElement(boxRef)
</script>
<style scoped>
.interactive-box {
width: 300px;
height: 200px;
border: 2px solid #ccc;
padding: 20px;
transition: background-color 0.3s;
}
.interactive-box.is-hover {
background-color: #e3f2fd;
}
</style>
4.4 [3]插件系统 (app.use())
01.插件系统 (app.use())
a.定义与用法
用 JS 插件、全局/局部自定义指令,将行为逻辑解耦于组件外部。适合与 DOM 或第三方库深度集成的公共能力。
优点: 逻辑高聚合,可多端共享,易配置发布
不足: 干预性过强时易增大学习和排查成本
b.自定义指令定义部分
a.directives/vFocus.js
export const vFocus = {
mounted(el) {
el.focus()
}
}
b.directives/vClickOutside.js
export const vClickOutside = {
mounted(el, binding) {
el.clickOutsideEvent = function(event) {
// 检查点击是否在元素外部
if (!(el === event.target || el.contains(event.target))) {
binding.value(event)
}
}
document.addEventListener('click', el.clickOutsideEvent)
},
unmounted(el) {
document.removeEventListener('click', el.clickOutsideEvent)
}
}
c.directives/index.js - 全局注册
import { vFocus } from './vFocus'
import { vClickOutside } from './vClickOutside'
export function registerDirectives(app) {
app.directive('focus', vFocus)
app.directive('click-outside', vClickOutside)
}
c.自定义指令使用部分
a.全局注册方式:main.js
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import { registerDirectives } from './directives'
const app = createApp(App)
// 全局注册所有自定义指令
registerDirectives(app)
app.mount('#app')
b.组件中使用
a.SearchInput.vue
<!-- SearchInput.vue -->
<template>
<div class="search-wrapper">
<!-- 使用 v-focus 指令 -->
<input
v-focus
v-model="searchText"
placeholder="自动获取焦点"
class="search-input"
/>
</div>
</template>
<script setup>
import { ref } from 'vue'
const searchText = ref('')
</script>
b.Dropdown.vue
<!-- Dropdown.vue -->
<template>
<div class="dropdown">
<button @click="isOpen = !isOpen">
下拉菜单
</button>
<!-- 使用 v-click-outside 指令 -->
<div
v-if="isOpen"
v-click-outside="closeDropdown"
class="dropdown-menu"
>
<ul>
<li>选项 1</li>
<li>选项 2</li>
<li>选项 3</li>
</ul>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const isOpen = ref(false)
function closeDropdown() {
isOpen.value = false
}
</script>
c.局部注册方式
<template>
<input v-focus placeholder="局部指令示例" />
</template>
<script setup>
// 在 <script setup> 中,以 v 开头的驼峰变量可作为自定义指令
const vFocus = {
mounted: (el) => el.focus()
}
</script>
4.5 [4]自定义指令 (directive)
01.自定义指令 (directive)
a.插件定义
// plugins/logger.js
export default {
install(app, options = {}) {
const prefix = options.prefix || '[App]'
// 添加全局方法
app.config.globalProperties.$log = function(message) {
console.log(`${prefix}`, message)
}
// 提供全局数据
app.provide('logger', {
log: (msg) => console.log(`${prefix}`, msg),
error: (msg) => console.error(`${prefix}`, msg)
})
// 注册全局指令
app.directive('log', {
mounted(el, binding) {
console.log(`${prefix} 元素挂载:`, binding.value)
}
})
}
}
b.插件使用
a.main.js
import { createApp } from 'vue'
import App from './App.vue'
import loggerPlugin from './plugins/logger'
const app = createApp(App)
// 注册插件
app.use(loggerPlugin, { prefix: '[MyApp]' })
app.mount('#app')
b.ComponentUsingPlugin.vue
<template>
<div v-log="'这个元素被渲染了'">
<button @click="handleClick">点击记录</button>
</div>
</template>
<script setup>
import { inject, getCurrentInstance } from 'vue'
// 方式1: 使用inject获取插件提供的功能
const logger = inject('logger')
// 方式2: 使用全局属性(Options API风格)
const instance = getCurrentInstance()
function handleClick() {
logger.log('按钮被点击')
// 或者: instance.proxy.$log('按钮被点击')
}
</script>
4.6 [5]Pinia/Vuex+Composables
01.Pinia/Vuex+Composables
a.定义与用法
逻辑拆分为 Pinia/Vuex store、router guards、外部业务 utils,交由组合函数组织与分发。适合大型项目的全局数据与副作用隔离。
优点: 可维护性高、利于模块化协作
不足: 项目小则架构沉重
b.Pinia Store 定义部分
a.stores/userStore.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
// 状态
const userInfo = ref(null)
const token = ref(localStorage.getItem('token') || '')
// 计算属性
const isLoggedIn = computed(() => !!token.value)
const username = computed(() => userInfo.value?.name || '游客')
// 方法
async function login(credentials) {
try {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials)
})
const data = await response.json()
token.value = data.token
userInfo.value = data.user
localStorage.setItem('token', data.token)
return { success: true }
} catch (error) {
return { success: false, error }
}
}
function logout() {
token.value = ''
userInfo.value = null
localStorage.removeItem('token')
}
return {
userInfo,
token,
isLoggedIn,
username,
login,
logout
}
})
c.结合 Composable 的 Store 使用模式
a.composables/useAuth.js
import { computed } from 'vue'
import { useUserStore } from '@/stores/userStore'
import { useRouter } from 'vue-router'
export function useAuth() {
const userStore = useUserStore()
const router = useRouter()
const isAuthenticated = computed(() => userStore.isLoggedIn)
const currentUser = computed(() => userStore.userInfo)
async function handleLogin(credentials) {
const result = await userStore.login(credentials)
if (result.success) {
router.push('/dashboard')
}
return result
}
function handleLogout() {
userStore.logout()
router.push('/login')
}
function requireAuth() {
if (!isAuthenticated.value) {
router.push('/login')
return false
}
return true
}
return {
isAuthenticated,
currentUser,
handleLogin,
handleLogout,
requireAuth
}
}
d.组件中使用解耦模式
a.LoginForm.vue
<template>
<div class="login-form">
<input v-model="email" type="email" placeholder="邮箱" />
<input v-model="password" type="password" placeholder="密码" />
<button @click="login" :disabled="loading">
{{ loading ? '登录中...' : '登录' }}
</button>
<p v-if="error" class="error">{{ error }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useAuth } from '@/composables/useAuth'
const { handleLogin } = useAuth()
const email = ref('')
const password = ref('')
const loading = ref(false)
const error = ref('')
async function login() {
loading.value = true
error.value = ''
const result = await handleLogin({
email: email.value,
password: password.value
})
if (!result.success) {
error.value = '登录失败,请检查账号密码'
}
loading.value = false
}
</script>
b.Dashboard.vue
<template>
<div class="dashboard">
<header>
<h1>欢迎, {{ currentUser?.name }}</h1>
<button @click="handleLogout">退出登录</button>
</header>
<main>
<!-- 仪表盘内容 -->
</main>
</div>
</template>
<script setup>
import { onMounted } from 'vue'
import { useAuth } from '@/composables/useAuth'
const { currentUser, handleLogout, requireAuth } = useAuth()
onMounted(() => {
// 路由守卫逻辑
requireAuth()
})
</script>
e.路由守卫结合示例
a.router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '@/stores/userStore'
const routes = [
{
path: '/login',
component: () => import('@/views/Login.vue')
},
{
path: '/dashboard',
component: () => import('@/views/Dashboard.vue'),
meta: { requiresAuth: true }
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// 全局前置守卫
router.beforeEach((to, from, next) => {
const userStore = useUserStore()
if (to.meta.requiresAuth && !userStore.isLoggedIn) {
next('/login')
} else {
next()
}
})
export default router
4.7 [6]局部 mixin:选项式API
01.Options API 的局部 mixin
a.定义与用法
在独立 JS 文件中导出一个对象,包含 data、methods、生命周期等选项,在特定组件通过 mixins: [myMixin] 混入。主要用于快速复用简单功能和兼容老项目。
优点: 简单易懂,老项目迁移成本低
不足: 命名冲突不可控,逻辑来源不透明,难以类型推导和维护
b.Mixin 定义部分
// mixins/loggerMixin.js
export const loggerMixin = {
data() {
return {
logPrefix: '[Logger]'
}
},
methods: {
logInfo(message) {
console.log(`${this.logPrefix} INFO: ${message}`)
},
logError(message) {
console.error(`${this.logPrefix} ERROR: ${message}`)
}
},
created() {
console.log(`${this.logPrefix} Component created at:`, new Date().toLocaleTimeString())
}
}
c.Mixin 使用部分
<!-- UserProfile.vue -->
<template>
<div class="user-profile">
<h2>{{ username }}</h2>
<button @click="handleAction">执行操作</button>
</div>
</template>
<script>
import { loggerMixin } from '@/mixins/loggerMixin'
export default {
name: 'UserProfile',
// 引入局部mixin
mixins: [loggerMixin],
data() {
return {
username: 'John Doe'
}
},
methods: {
handleAction() {
this.logInfo('用户执行了操作')
// 业务逻辑...
}
},
mounted() {
this.logInfo('用户资料组件已挂载')
}
}
</script>
4.8 [7]全局 mixin
01.全局 mixin
a.定义与用法
使用应用实例的 app.mixin() 或全局 API 注册,可作用于所有组件。适用于全局注入通用逻辑(如国际化、日志收集),风险高。
优点: 适合统一全局逻辑
不足: 影响全局,易污染和冲突,常被视为"黑魔法"
b.全局 Mixin 定义部分
// main.js
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
// 定义全局mixin
app.mixin({
data() {
return {
globalTimestamp: Date.now()
}
},
methods: {
$globalLog(message) {
console.log('[Global Mixin]', message)
}
},
created() {
// 所有组件创建时都会执行
console.log('全局mixin: 组件已创建', this.$options.name)
}
})
app.mount('#app')
c.全局 Mixin 使用部分
<!-- AnyComponent.vue -->
<template>
<div>
<p>全局时间戳: {{ globalTimestamp }}</p>
<button @click="useGlobalMethod">调用全局方法</button>
</div>
</template>
<script>
export default {
name: 'AnyComponent',
// 无需显式引入,全局mixin自动生效
methods: {
useGlobalMethod() {
// 直接使用全局mixin中定义的方法
this.$globalLog('这是来自组件的消息')
}
},
mounted() {
// 可直接访问全局mixin的data
console.log('访问全局数据:', this.globalTimestamp)
}
}
</script>
5 plugin
5.1 汇总
01.常用信息
a.核心概念速查表
概念 | 定义 | 使用场景 | 对比
----------------|----------------------------|-----------------------|-------------------------------------------
Plugin | 具有install方法的对象或函数 | 扩展Vue全局功能 | vs Mixin:插件是应用级,mixin是组件级
install() | 插件的核心入口函数 | 定义插件要做什么 | 必须实现
app.use() | 安装插件的方法 | 注册插件到Vue应用 | 有防重复机制
app.component() | 全局注册组件 | 注册全局组件 | vs app.use():组件专用
app.mixin() | 应用全局混入 | 注入到所有组件 | 不推荐(用Composables)
b.插件是什么?(JS角度理解)
a.本质:设计模式中的"装饰器"或"工厂模式"
// ❌ 不用插件的方式
const app = createApp(App)
// 每个地方都要手动配置
app.config.globalProperties.$log = () => {}
app.config.globalProperties.$notify = () => {}
app.directive('focus', {})
// ...重复配置
// ✅ 用插件的方式:把所有配置包装成一个插件
const myPlugin = {
install(app, options) {
// 所有配置集中在这里
app.config.globalProperties.$log = () => {}
app.config.globalProperties.$notify = () => {}
app.directive('focus', {})
}
}
app.use(myPlugin) // 一句话搞定!
b.核心思想
把需要重复做的事情包装成一个可复用的"插件",其他项目直接拿来用。
c.快速参考
// 最简单的插件
export default {
install(app) {
app.config.globalProperties.$hello = () => alert('Hello!')
}
}
// 使用
const app = createApp(App)
app.use(myPlugin)
// 在组件中
this.$hello() // Options API
instance?.proxy?.$hello() // Composition API
02.app.use()的实现原理
a.源码伪代码
// Vue内部的app.use()实现思路
class Application {
constructor() {
this._installedPlugins = [] // 记录已安装的插件
}
use(plugin, options = {}) {
// 1. 去重检查:防止重复安装
if (this._installedPlugins.includes(plugin)) {
console.warn('插件已安装,跳过重复安装')
return this
}
// 2. 类型检查:插件可以是对象或函数
if (typeof plugin === 'function') {
// 如果是函数,直接调用
plugin(this, options)
} else if (plugin && typeof plugin.install === 'function') {
// 如果是对象,调用install方法
plugin.install(this, options)
} else {
console.warn('插件必须有install方法或本身是函数')
return this
}
// 3. 记录已安装的插件
this._installedPlugins.push(plugin)
// 4. 返回this,支持链式调用
return this
}
}
b.关键特性
1.防重复安装(已安装过的插件不会再装)
2.链式调用(app.use(p1).use(p2).use(p3))
3.灵活的形式(对象或函数都可以)
5.2 [1]写法:3种
00.汇总
写法1:对象形式(推荐)
写法2:函数形式
写法3:完整功能插件(综合示例)
01.写法1:对象形式(推荐)
// plugins/logger.js
export default {
install(app, options = {}) {
const prefix = options.prefix || '[Log]'
// 添加全局属性/方法
app.config.globalProperties.$log = (message) => {
console.log(`${prefix}`, message)
}
}
}
// main.js
import loggerPlugin from './plugins/logger'
const app = createApp(App)
app.use(loggerPlugin, { prefix: '[MyApp]' })
02.写法2:函数形式
// plugins/notify.js
export default function notifyPlugin(app, options = {}) {
const timeout = options.timeout || 3000
app.config.globalProperties.$notify = (message) => {
console.log(`通知: ${message}`)
// 实际项目中这里会调用UI库显示通知
}
}
// main.js
import notifyPlugin from './plugins/notify'
const app = createApp(App)
app.use(notifyPlugin, { timeout: 5000 })
03.写法3:完整功能插件(综合示例)
// plugins/myPlugin.js
export default {
install(app, options = {}) {
// 1️⃣ 添加全局属性和方法
app.config.globalProperties.$myConfig = {
version: '1.0.0',
author: 'John'
}
// 2️⃣ 添加全局方法
app.config.globalProperties.$formatDate = (date) => {
return new Date(date).toLocaleDateString()
}
// 3️⃣ 注册全局组件
app.component('Loading', {
template: `<div>加载中...</div>`
})
// 4️⃣ 注册全局指令
app.directive('focus', {
mounted(el) {
el.focus()
}
})
// 5️⃣ 应用全局混入(不推荐,但有时需要)
app.mixin({
created() {
console.log('每个组件都会执行这个created')
}
})
// 6️⃣ 提供依赖注入
app.provide('myService', {
getUser: () => ({ name: 'John' })
})
}
}
5.3 [2]组件使用:4种
00.汇总
方式1:使用全局属性
方式2:使用全局组件
方式3:使用全局指令
方式4:使用provide/inject
01.方式1:使用全局属性
<template>
<div>
<button @click="handleLog">打印日志</button>
<p>{{ formattedDate }}</p>
</div>
</template>
<script setup>
import { getCurrentInstance, computed } from 'vue'
const instance = getCurrentInstance()
// ✅ Options API方式
// 在method中使用:this.$log('消息')
// ✅ Composition API方式
function handleLog() {
// 获取全局属性
instance?.proxy?.$log('这是一条日志')
}
const formattedDate = computed(() => {
return instance?.proxy?.$formatDate(new Date())
})
</script>
02.方式2:使用全局组件
<template>
<!-- 直接使用,无需import -->
<Loading />
</template>
03.方式3:使用全局指令
<template>
<!-- 输入框自动获焦 -->
<input v-focus placeholder="自动获焦" />
</template>
04.方式4:使用provide/inject
<script setup>
import { inject } from 'vue'
// 从插件的provide中注入依赖
const myService = inject('myService')
function getUser() {
const user = myService.getUser()
console.log(user)
}
</script>
5.4 [3]发布插件:流程
01.推荐
// 1. 参数验证
export default {
install(app, options = {}) {
if (!options.required) {
console.warn('[MyPlugin] 缺少必需配置')
}
}
}
// 2. 错误捕获
export default {
install(app, options = {}) {
try {
// 插件逻辑
} catch (error) {
console.error('[MyPlugin] 初始化失败:', error)
throw error
}
}
}
// 3. 防污染:使用前缀或命名空间
app.config.globalProperties.$myPlugin = {} // ✅ 带插件标识
// 4. 支持链式调用
app.use(plugin1).use(plugin2).use(plugin3)
02.避免
// 1. 全局污染:直接添加全局属性
app.config.globalProperties.foo = {} // ❌ 容易冲突
// 2. 在install中修改用户数据
app.mixin({
data() {
return { count: 0 } // ❌ 影响所有组件
}
})
// 3. 同步安装异步功能
// ❌ 不要在install中启动异步操作
export default {
install(app) {
fetch('/config').then(...) // 不确定何时完成
}
}
03.发布插件到npm
a.package.json
{
"name": "my-vue3-plugin",
"version": "1.0.0",
"main": "dist/index.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"peerDependencies": {
"vue": "^3.0.0"
}
}
b.命令
# 构建
npm run build
# 发布
npm publish
5.5 完整的HTTP客户端插件
01.示例:完整的HTTP客户端插件
a.定义插件
// plugins/http.js
export default {
install(app, options = {}) {
const baseURL = options.baseURL || '/api'
const timeout = options.timeout || 5000
// 创建HTTP客户端
const httpClient = {
async get(url, config = {}) {
try {
const response = await fetch(`${baseURL}${url}`, {
method: 'GET',
timeout,
...config
})
return await response.json()
} catch (error) {
console.error('HTTP GET error:', error)
throw error
}
},
async post(url, data, config = {}) {
try {
const response = await fetch(`${baseURL}${url}`, {
method: 'POST',
timeout,
body: JSON.stringify(data),
...config
})
return await response.json()
} catch (error) {
console.error('HTTP POST error:', error)
throw error
}
}
}
// 注册为全局属性
app.config.globalProperties.$http = httpClient
// 也可以通过provide/inject的方式
app.provide('http', httpClient)
}
}
b.在main.js中注册
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import httpPlugin from './plugins/http'
const app = createApp(App)
// 注册HTTP插件
app.use(httpPlugin, {
baseURL: 'https://api.example.com',
timeout: 10000
})
app.mount('#app')
c.在组件中使用
<script setup>
import { ref, getCurrentInstance } from 'vue'
const users = ref([])
const loading = ref(false)
const instance = getCurrentInstance()
async function fetchUsers() {
loading.value = true
try {
// ✅ 方式1:通过全局属性
users.value = await instance?.proxy?.$http.get('/users')
// 或者 ✅ 方式2:通过inject
// const http = inject('http')
// users.value = await http.get('/users')
} catch (error) {
console.error('获取用户失败:', error)
} finally {
loading.value = false
}
}
</script>
<template>
<div>
<button @click="fetchUsers" :disabled="loading">
{{ loading ? '加载中...' : '加载用户' }}
</button>
<ul>
<li v-for="user in users" :key="user.id">{{ user.name }}</li>
</ul>
</div>
</template>