Skip to main content

组件通讯

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

image-20211216153245716

族谱📜:

  • 父子关系:A和AA,AA和AAA,B和BB、BB和BBB
  • 兄弟关系:A和B
  • 爷孙关系:A和AAA、B和BBB
  • 邻居关系:A和B内任意,B与A内任意

常用的通讯方式

名称说明
Prop 和 $emit单向数据流,官方推荐,最基础也是最常用的传输手段
eventBus事件总线,耦合低,但是用多了会让业务代码分散到各处,需要另外定制一些团队规则
vuex官方出品的全局状态管理库
pinia新一代的vue状态管理(2021年末)

不常用的通讯方式(不推荐)

名称说明
$parent 、 $children直接访问父子实例上的方法或者属性,感觉比较不安全,少用
$refs用来操作DOM常用,其实也可以用来跟上下文的组件通讯,也是少用
provide 、 inject2.2 新增,官方不建议用在日常业务组件中,建议用在组件库封装时
$attrs 、 $listeners、inheritAttrs2.4 新增,官方推荐在一些组件封装的地方使用,日常用会有重复触发事件的坑,需要去重

PS:虽然方法有这么多,但是团队中每次开发项目应该统一指定一种到两种主要通讯方式,这样方便后续项目维护和升级。

不推荐的方式有的是淘汰的,有的是不便于团队后期维护的,用是能用,但是多少有点门槛或者要给项目带来额外的维护成本。

1、Prop 、 $emit

单向数据流

父组件通过绑定同名的props参数,将数据向子组件传递,通过监听子组件的emit触发的事件来更新自身数据,这个过程是单向的,也称为单向数据流

vue2

<template lang="pug">
Child1(v-bind:param1="param1")
</template>
<script>
import Vue from "Vue"
let parent = new Vue({
el:'#app',
name:"parent",
data(){
return{
param1:"1",
param2:"2"
}
},
method:{
// 父组件修改数据暴露的接口
parentChangeData({param, newValue}){
this[param] = newValue
}
}
components:{
'Child1':{
props:['param1'],
template:`<div>子组件:{{param1}}</div>`,
method:{
changeData(){
// 使用emit触发父组件暴露的方法来修改数据
this.$emit('parentChangeData',{})
}
}
}
}
})
</script>

vue3

<template>
<button
class="cursor-pointer text-black hover:text-sky-400"
@click="$emit('on-menu-click', menu)"
></button>
</template>
<script setup lang="ts">

</script>

2、$parent 、 $children

参阅资料:$parent/$children的使用场景

如何使用

<template lang="pug">
Child1(v-bind:param1="param1")
</template>
export default {
name:"parent-component",
data(){
return{
parentData:"data",
}
},
components:{
'Child1':{
template:`<div>子组件:{{param1}}</div>`,
beforeMount(){
this.getParentData()
},
data (){
return{
childData:"",
}
},
method:{
getParentData(){
this.childData = this.$parent.parentData
}
}
}
}
}

注意事项

  • 渲染的时候先子组件的mounted,然后再是自己的mounted要是想在子组件里获取父组件的元素之类的,必须使用nextTick
  • $children 并不保证顺序,也不是响应式的。**如果你发现自己正在尝试使用 $children 来进行数据绑定,考虑使用一个数组配合 v-for 来生成子组件,并且使用 Array 作为真正的来源。

适用场景

  • 封装嵌套组件时,直接使用长辈或者子孙组件的方法,该方法并不改变数据,常常结合$broadcast/$dispatch使用(官方已弃用)

3、$attrs 、 $listeners、inheritAttrs

vue3移除$listeners

场景限制 2.4 二次封装

参阅资料:Vue使用$attrs与$listeners实现多层嵌套传递

用法

<component v-bind="$attrs" v-on="$listeners"></component>

传统父组件要传递属性给孙组件需要先经过子组件定义props来逐层传递,在高层级的业务场景下非常不方便,而使用$attrs可以做到父组件直接传递到下级多层组件中,大大减少了层级带来的逐层传递问题关系。

<template lang="pug">
Child1(v-bind:param1="param1")
</template>

<script>
/* 顶层组件 */
let parent = new Vue({
el:'#app',
name:"parent",
data(){
return{
param1:"1",
param2:"2",
}
},
methods:{
// 顶层组件暴露一个修改属性的方法
changeData(newData){
this.param2 = newData
}
}
/* 中层组件 */
components:{
'Child1':{
// 中层组件不需重复定义props,只需要添加固定的 v-bind="$arrts"、v-on="$listeners"
template:`<Child2 v-bind="$arrts" v-on="$listeners"></Child2>`,

/* 下层组件 */
components:{
'Child2':{
props:['param2'] // 下层组件依然就能得到param2,和触发changeData
template:`<div>我是孙组件我们直接使用param1 {{param1}}</div>`,
methods:{
setParentData(){
// 下层组件直接能使用 $emit 来触发顶层组件的方法
this.$emit('changeData', this.param2)
}
}
}
}
}
}
})
</script>

  • 官方推荐 $listener 的应用场景是二次方状的组件上,所以非必要尽量不要采用此种方式作为组件通讯的首选

4、provide 和 inject (2.2 加入)

  • 父组件
// 父组件通过provide字段定义事件
export default {
mounted() {},
data:(){},

provide: { 'eventName':()=>{}, 'data':'ccvb'}
}
  • 子组件
// 子组件通过 inject调用
export default {
// 通过 this+时间名称调用,支持函数或者变量
created(){
this.eventName()
}

inject: ['eventName', 'data']
}

5、$refs 操作组件实例

6、eventBus 事件总线(推荐)

事件总线是官方推荐的其从一套组件通讯解决方案:点我跳转()

vue2

// @/eventBus.ts
import Vue from 'vue'
export default new Vue()
import eventBus from '@libs/eventBus'

export default {
mounted() {
// 在组件创建时,添加一个名为 `hello` 的事件侦听
eventBus.$on('hello', () => {
console.log('Hello World')
})
},
beforeDestroy() {
// 在组件销毁前,通过 `hello` 这个名称移除该事件侦听
eventBus.$off('hello')
},
}
import eventBus from './eventBus'

export default {
methods: {
sayHello() {
// 触发名为 `hello` 的事件
eventBus.$emit('hello')
},
},
}

vue3

使用第三方时间管理库mitt

// src/libs/eventBus.ts
import mitt from 'mitt'
export default mitt()
import { defineComponent, onBeforeUnmount } from 'vue'
import eventBus from '@libs/eventBus'

export default defineComponent({
setup() {
// 声明一个打招呼的方法
function sayHi(msg = 'Hello World!') {
console.log(msg)
}

// 启用侦听
eventBus.on('sayHi', sayHi)

// 在组件卸载之前移除侦听
onBeforeUnmount(() => {
eventBus.off('sayHi', sayHi)
})
},
})
import { defineComponent } from 'vue'
import eventBus from '@libs/eventBus'

export default defineComponent({
setup() {
// 调用打招呼事件,传入消息内容
eventBus.emit('sayHi', 'Hello')
},
})

reactive(适用于一些临时页面,临时组件)

兄弟组件间或者组件内部多层级数据状态共享时,适合采用

// @/xxxState.ts
import { reactive } from 'vue';

// 定义为唯一导出
export default reactive({
// 设置一个属性并赋予初始值
message: 'Hello World',

// 添加一个更新数据的方法
setMessage(msg: string) {
this.message = msg;
},
});

// 具名导出,相当于一个极简单的全局状态管理
export const xxxxStroe = reactive({})
// xxx.vue
import { defineComponent, watch } from 'vue'
import { state } from '@/xxxState'

export default defineComponent({
setup() {
console.log(state.message)
// Hello World

// 因为是响应式数据,所以可以侦听数据变化
watch(
() => state.message,
(val) => {
console.log('Message 发生变化:', val)
}
)

setTimeout(() => {
state.setMessage('Hello Hello')
// Message 发生变化: Hello Hello
}, 1000)

setTimeout(() => {
state.message = 'Hi Hi'
// Message 发生变化: Hi Hi
}, 2000)
},
})

优点:

  • 使用简单,轻量
  • 合适使用在一个组件或者插件中,进行局部的状态管理

缺点:

  • 难以追踪,不合适用来全局,会增加维护成本

7、vuex

总结