组件通讯
📜通讯族谱
组件与组件间常出现的关系:
- 父子关系:A和AA,AA和AAA,B和BB、BB和BBB
- 兄弟关系:A和B
- 爷孙关系:A和AAA、B和BBB
- 邻居关系:A和B内任意,B与A内任意
通讯方式
vue2
名称 | 说明 |
---|---|
Prop | 单向数据流,官方推荐,最基础也是最常用的传输手段 |
Prop和 $emit | |
eventBus | 事件总线,耦合低,但是用多了会让业务代码分散到各处,需要另外定制一些团队规则 |
vuex / pinia | 官方出品的全局状态管理库 |
$parent 、 $children | 直接访问父子实例上的方法或者属性,感觉比较不安全,少用 |
$refs | 用来操作DOM常用,其实也可以用来跟上下文的组件通讯,也是少用 |
provide 、 inject | 2.2 新增,官方不建议用在日常业务组件中,建议用在组件库封装时 |
$attrs 、 $listeners、inheritAttrs | 2.4 新增,官方推荐在一些组件封装的地方使用,日常用会有重复触发事件的坑,需要去重 |
PS:虽然方法有这么多,但是团队中每次开发项目应该统一指定一种到两种主要通讯方式,这样方便后续项目维护和升级。
不推荐的方式有的是淘汰的,有的是不便于团队后期维护的,用是能用,但是多少有点门槛或者要给项目带来额外的维护成本。
vue3
名称 | 说明 | 通讯范围 |
---|---|---|
Prop | 单向数据流,官方推荐,最基础也是最常用的传输手段 | 父子 |
Prop和 $emit | 父子 | |
expose / ref | 父子、爷孙 | |
v-model | ||
provide / inject | ||
EventBus | ||
Reative State | ||
vuex / pinia |
纯Props (单向)
vue2
- 子
<template >
<div>子组件:{{param1}}</div>
</template>
<script>
export default {
props:['param1'],
method:{
changeData(){
// 子组件要修改父组件的数据
this.$emit('changeParentData',{})
}
}
}
</script>
- 父
<template lang="pug">
Child1(v-bind:param1="param1")
</template>
<script>
export default {
name:"parent",
components:{Child1}
data(){
return{
param1:"1",
param2:"2"
}
},
method:{
// 父组件修改数据暴露的接口
changeParentData({param, newValue}){
this[param] = newValue
}
}
})
</script>
Prop & $emit (双向)
单向数据流
父组件通过绑定同名的props参数,将数据向子组件传递,通过监听子组件的emit触发的事件来更新自身数据,这个过程是单向的,也称为单向数据流
Input双向绑定
- 子
// Child.vue
<template>
<input type="text" :value="value" @input="onInput"/>
</template>
<script setup lang="ts">
const emit = defineEmits<{
(event: 'update:value', newValue: string): void;
}>();
const props = withDefaults(
defineProps<{
label?: string;
value: string;
}>(),
{ label: () => '标题: ' }
);
const onInput = (e: any) => {
emit('update:value', e.target.value);
};
</script>
- 父
<template>
<div>父组件:{{ message }}</div>
<!-- 进行双向绑定 -->
<Child v-model:value="inputValueRef" />
</template>
<script setup lang="ts">
const inputValueRef = ref('')
</script>
Emit 方法绑定
- 子
// Child.vue
<template>
<div> {{ msg }} </div>
<button @click="handleClick">子组件的按钮</button>
</template>
<script setup>
const props = defineProps( {msg: { type: String, default: '' }} )
// 注册一个自定义事件名,向上传递时告诉父组件要触发的事件。
const emit = defineEmits(['changeMsg'])
function handleClick() {
// 参数1:事件名
// 参数2:传给父组件的值
emit('changeMsg', 'newMsg')
}
</script>
- 父
<template>
<div>父组件:{{ message }}</div>
<!-- 自定义 changeMsg 事件 -->
<Child @changeMsg="changeMessage" />
</template>
<script setup>
import { ref } from 'vue'
import Child from './components/Child.vue'
let message = ref('雷猴')
// 更改 message 的值,data是从子组件传过来的
function changeMessage(data) {
message.value = data
}
</script>
ref & defineExpose
defineExpose
只能在setp script
中使用,否则无法生效
vue3-js
- Child.vue
<template>
<div> {{ msg }} </div>
</template>
<script setup>
// 注册一个自定义事件名,向上传递时告诉父组件要触发的事件。
const msg = ref('xxxx')
function setMsg(newMsg){
msg.value = newMsg
}
// 关键! 这里声明向外部提供暴露这两个属性
defineExpose({ message, setMsg })
</script>
- Parent.vue
<template>
<button @click="callChildFn">调用子组件的方法</button>
<Child ref="child" />
</template>
<script setup>
import { ref, onMounted } from 'vue'
import Child from './components/Child.vue'
const child = ref(null) // 通过 模板ref 绑定子组件
onMounted(() => {
// 在加载完成后,将子组件的 message 赋值给 msg
msg.value = child.value.msg
})
function callChildFn() {
// 调用子组件的 setMsg 方法
child.value.setMsg('xxxx')
}
</script>
vue3-ts (推荐)
Child.vue
<template>
<div>子组件内容: {{ msg }}</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const msg = ref('子组件的初始消息')
const setMsg = (newMsg: string) => {
msg.value = newMsg
}
// 显式暴露给父组件的属性或方法
defineExpose({
msg,
setMsg
})
</script>
Parent.vue
<template>
<button @click="callChildFn">调用子组件的方法</button>
<Child ref="childRef" />
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import Child from './components/Child.vue'
// 使用 ref 指令来引用子组件
const childRef = ref<InstanceType<typeof Child> | null>(null)
// 确保在 DOM 挂载后访问子组件的属性或方法
onMounted(() => {
if (childRef.value) {
// 在加载完成后,将子组件的 message 赋值给 msg(但通常你不会在父组件中直接操作子组件的数据)
// 这里仅作演示,你可能需要定义一个响应式 ref 在父组件中
console.log(childRef.value.msg)
}
})
// 调用子组件的方法
function callChildFn() {
if (childRef.value) {
childRef.value.setMsg('xxxx')
}
}
</script>
注意几点:
InstanceType<typeof Child>
是 TypeScript 的一个类型工具,用于获取组件实例的类型。这是因为你通过ref
获取到的是子组件的实例,而不是它的构造函数。defineExpose
用于显式地定义子组件暴露给模板或父组件的属性或方法。尽管在<script setup>
中,顶层的响应式状态和方法默认就是暴露的,但使用defineExpose
可以使意图更加明确。- 在父组件中,你不需要显式地创建
ref
变量来存储子组件的引用,因为 Vue 3 会自动为你处理。但是,为了类型安全,你可以使用ref<InstanceType<typeof Child> | null>(null)
来指定ref
的类型。 - 在调用子组件的方法或访问其属性之前,请确保子组件已经挂载(即在
onMounted
钩子中或之后进行)。否则,childRef.value
可能是null
。
v-model & emit (双向)
vue3 (3.4之后 defineModel)
父组件
<template>
<CommonInput v-model="inputValue" />
</template>
<script setup lang="ts">
import { ref } from "vue";
const inputValue = ref();
</script>
子组件
<template>
<input v-model="model" />
</template>
<script setup lang="ts">
const model = defineModel();
model.value = "xxx";
</script>
vue3(3.4之前)
父组件
<script setup lang="ts">
import Son from "./test.vue"
const count = ref(10)
const rect = reactive({
x: 1,
y: 1,
})
</script>
<template>
<div>
<son
:rect="rect"
@update:rect="rect = $event"
:model-value="count"
@update:modelValue="count = $event"
></son>
<son v-model:model-value="count" v-model:rect="rect"></son>
</div>
</template>
写法1:defineEmits
<script setup lang="ts">
// 计数器
// 通过 v-model 解析成 modelValue @update:modelValue
const props = defineProps<{
modelValue: number
rect: {
x: number
y: number
}
}>()
const emits = defineEmits<{
(e: "update:modelValue", count: number): void
(e: "update:rect", coords: { x: number; y: number }): void
}>()
function test() {
emits("update:modelValue", props.modelValue + 1)
emits("update:rect", { x: props.modelValue + 1, y: props.modelValue + 1 })
}
</script>
<template>
<div class="cp-radio-btn">
计数器:{{ modelValue }} ----
<button @click="test">点击加1</button>
<div>
{{ rect }}
</div>
</div>
</template>
写法2:$emit
<template>
<div @click="$emit('update:modelValue', 'newValue')">{{ modelValue }}</div>
</template>
<script setup>
import { ref } from 'vue'
// 接收父组件使用 v-model 传进来的值,必须用 modelValue 这个名字来接收
const props = defineProps([ 'modelValue' ])
</script>
写法3:传统
<script>
export default defineComponent({
props: {
uid: Number,
username: String,
age: Number,
},
// 注意这里的 `update:` 前缀
emits: ['update:uid', 'update:username', 'update:age'],
setup(props, { emit }) {
// 2s 后更新用户名
setTimeout(() => {
emit('update:username', 'Tom')
}, 2000)
},
})
</script>
写法4:修饰符
常用几个修饰符:
内置修饰符 | 说明s | |
---|---|---|
.once | ||
.sayc | ||
父组件:
<template>
<Child v-model.uppercase="message" />
</template>
<script setup>
import { ref } from 'vue'
import Child from './components/Child.vue'
const message = ref('hello')
</script>
子组件:
<template>
<div>{{modelValue}}</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const props = defineProps([
'modelValue',
'modelModifiers': { default: () => ({}) }
])
const emit = defineEmits(['update:modelValue'])
onMounted(() => {
// 判断有没有 自定义的修饰符 修饰符,有的话就执行 toUpperCase() 方法
if (props.modelModifiers.{自定义修饰符}) {
emit('update:modelValue', props.modelValue.toUpperCase())
}
})
</script>
provide & inject
vue3
- 父组件
import { defineComponent, provide, ref } from 'vue'
export default defineComponent({
setup() {
// 声明一个响应性变量并 provide 其自身
// 孙组件获取后可以保持响应性
const msg = ref('Hello World!')
provide('msg', msg)
// 只 provide 响应式变量的值
// 孙组件获取后只会得到当前的值
provide('msgValue', msg.value)
// 声明一个方法并 provide
function printMsg() {
console.log(msg.value)
}
provide('printMsg', printMsg)
},
})
- 子组件
import { defineComponent, inject } from 'vue'
import type { Ref } from 'vue'
export default defineComponent({
setup() {
// 获取响应式变量
const msg = inject('msg', 'defaultMsg') as Ref<string>
console.log(msg!.value)
// 获取普通的字符串
const msgValue = inject('msgValue', 'defaultMsgValue') as string
console.log(msgValue)
// 获取函数
const printMsg = inject('printMsg', 'defaultPrintMsg') as () => void
},
})
vue2
- 父组件
export default {
// 在 `data` 选项里定义好数据
data() {
return {
tags: ['中餐', '粤菜', '烧腊'],
}
},
// 在 `provide` 选项里添加要提供的数据
provide() {
return {
tags: this.tags,
}
},
}
- 子组件
export default {
// 通过 `inject` 选项获取
inject: ['tags'],
mounted() {
console.log(this.tags)
},
}
EventBus事件总线
常用的方案有:
方案 | Vue2.x | Vue3.x |
---|---|---|
使用传统Vue实例 | ✔️ | ⛔ |
使用官方状态库Vuex、pinia | ✔️ | ✔️ |
vueUse的useEventBus | ⛔ | ✔️ |
mitt 第三方事件驱动库 | ✔️ | ✔️ |
- 如果担心项目以后可能会从vue2升级到vue3,建议使用
miit
配合LocalStore
,大中小体量项目都适用 - Vue3的中小项目可以用
useEventBus
配合LocalStore
,中大体量使用官方状态库Vuex/pina
较为稳妥 - 个人项目建议混搭,可以熟悉多个时间总线技术