Skip to main content

组件通讯

📜通讯族谱

image-20211216153245716

组件与组件间常出现的关系:

  • 父子关系: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 、 inject2.2 新增,官方不建议用在日常业务组件中,建议用在组件库封装时
$attrs 、 $listeners、inheritAttrs2.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>

注意几点:

  1. InstanceType<typeof Child> 是 TypeScript 的一个类型工具,用于获取组件实例的类型。这是因为你通过 ref 获取到的是子组件的实例,而不是它的构造函数。
  2. defineExpose 用于显式地定义子组件暴露给模板或父组件的属性或方法。尽管在 <script setup> 中,顶层的响应式状态和方法默认就是暴露的,但使用 defineExpose 可以使意图更加明确。
  3. 在父组件中,你不需要显式地创建 ref 变量来存储子组件的引用,因为 Vue 3 会自动为你处理。但是,为了类型安全,你可以使用 ref<InstanceType<typeof Child> | null>(null) 来指定 ref 的类型。
  4. 在调用子组件的方法或访问其属性之前,请确保子组件已经挂载(即在 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.xVue3.x
使用传统Vue实例✔️
使用官方状态库Vuex、pinia✔️✔️
vueUse的useEventBus✔️
mitt 第三方事件驱动库✔️✔️
  • 如果担心项目以后可能会从vue2升级到vue3,建议使用miit配合LocalStore,大中小体量项目都适用
  • Vue3的中小项目可以用useEventBus配合LocalStore,中大体量使用官方状态库Vuex/pina较为稳妥
  • 个人项目建议混搭,可以熟悉多个时间总线技术