1. 组件通信的概念
组件是Vue最强大的功能之一,vue中的每一个.vue
文件都可以视之为一个组件。在古代,人们通过驿站、飞鸽传书、烽火报警、符号、语言、眼神、触碰等方式进行信息传递,到了今天,随着科技水平的飞速发展,通信基本完全利用有线或无线电完成,相继出现了有线电话、固定电话、无线电话、手机、互联网甚至视频电话等各种通信方式。
从上面这段话,我们可以看到通信的本质是信息同步,共享回到vue中,每个组件之间的都有独自的作用域,组件间的数据是无法共享的但实际开发工作中我们常常需要让组件之间共享数据,这也是组件通信的目的要让它们互相之间能进行通讯,这样才能构成一个有机的完整系统。
2.组件通信的分类
组件间通信的分类可以分成以下:
- 父子组件之间的通信
- 兄弟组件之间的通信
- 祖孙与后代组件之间的通信
- 非关系组件间之间的通信
关系如下图:
3. 组件通信的方法
Vue3与Vue2 组件通信的区别:
- 移出事件总线,使用
mitt
代替。 vuex
换成了pinia
。- 把
.sync
优化到了v-model
里面了。 - 把
$listeners
所有的东西,合并到$attrs
中了。 $children
被砍掉了。
组件通信常见的搭配形式:
3.1 props
props是使用频率最高的通信方式,常用于父 <——>子 ,父子互传
如果是父传子:属性值是非函数。
如果是子传父:属性值是函数。其实就是父组件先传一个函数给子组件,子组件在调用父组件给的函数传递数据给父组件。
在声明 props
选项时,我们可以使用 defineProps API
,它们将自动地在 <script setup>
中可用:
体验一下代码吧:
//父组件 Father.vue
<template><div class="father"><h3>父组件,</h3><h4>我的车:{{ car }}</h4><h4>儿子给的玩具:{{ toy }}</h4><Child :car="car" :getToy="getToy"/></div>
</template><script setup lang="ts">import Child from './Child.vue'import { ref } from "vue";// 数据const car = ref('奔驰')const toy = ref()// 方法function getToy(value:string){toy.value = value}
</script>
//子组件Child.vue
<template><div class="child"><h3>子组件</h3><h4>我的玩具:{{ toy }}</h4><h4>父给我的车:{{ car }}</h4><button @click="getToy(toy)">玩具给父亲</button></div></template><script setup lang="ts">import { ref } from "vue";const toy = ref('奥特曼')defineProps(['car','getToy'])</script>
3.2 自定义事件
自定义事件常用与 子 =》父
Vue自定义事件是基于Vue实例的defineEmits()
方法和父组件的v-on
指令来实现的。首先,在父组件中监听自定义事件:
//组件代码 Father.vue
<template><div class="father"><!--给子组件child绑定事件--><Child @custom-event = "handleCustomEvent"></Child><p>来自子组件的数据:{{ reactiveData }}</p></div>
</template><script setup lang="ts">import Child from './Child.vue'import { ref } from "vue";let reactiveData = ref("");function handleCustomEvent(data:string){reactiveData.value = data;}
</script>
在父组件内,我们写了一个custom-event
自定义事件,只要触发这个事件就可以调用handleCustomEvent()
方法。在javascript原生事件中click
事件,我们知道点击一下就会触发click
事件,那么custom-event
自定义事件自定义事件如何触发呢?接下来,需要我们在子组件中通过defineEmits()方法来申明事件触发custom-event自定义事件,代码如下:
//子组件代码 Child.vue
<template><button @click="emit('custom-event','我是子组件数据')">触发事件</button>
</template><script setup lang="ts">const emit = defineEmits(['custom-event'])
</script>
可以看到在子组件中通过defineEmits(['custom-event'])
得到emit
,再通过国emit('custom-event')
触发父组件绑定的custom-event自定义事件,并且将传递的参数自动注入到handleCustomEvent()
方法中。这样就可以通过自定义事件实现子组件到父组件的通信。
在组件的模版表达式中,可以直接使用 $emit 方法触发自定义事件,例如:
<template><button @click="$emit('custom-event','我是子组件数据')">触发事件</button>
</template>
3.3 mitt
在Vue2中使用事件总线 EventBus进行组件通信,Vue3中移除了EventBus,使用mitt进行代替。与消息订阅与发布功能类似,可以实现任意组件间的通信。具体使用步骤如下:
- 安装mitt
npm install mitt --save
- 建立工具文件夹utils 下新建emitter.ts /src/utils/emitter.ts
// 引入mitt
import mitt from "mitt";// 创建emitter
const emitter = mitt()// 创建并暴露mitt
export default emitter
- 在接收数据的组件:绑定事件、同时在销毁前解绑事件
import emitter from "@/utils/emitter";
import { onUnmounted } from "vue";// 绑定事件
emitter.on('send-toy',(value)=>{console.log('send-toy事件被触发',value)
})
onUnmounted(()=>{// 解绑事件emitter.off('send-toy')
})
- 在提供数据的组件,在合适的时候触发事件
import emitter from "@/utils/emitter";function sendToy(){// 触发事件emitter.emit('send-toy',toy.value)
}
3.4 v-model
v-model实现组件通信在业务当中用到的比较少,在封装UI组件库的时候会经常用到。我们回忆一下v-model在html标签上是如何使用的:
<input type='text' v-model="username" />
v-mode写在input标签上实现了数据的双向绑定,实际上面代码的底层是这样的:
<input type='text' :value="username" @input="username = (<HTMLInputElement>$event.target).value" />
写个完整的Father.vue组件运行试试吧:
<template><!--v-model用在html标签上--><!-- <input type="text" v-model="username"> --><input type="text" :value="username" @input="username = (<HTMLInputElement>$event.target).value"><p>{{ username }}</p>
</template><script setup lang="ts">import Child from './Child.vue'import {ref} from 'vue'let username = ref('zhangsan')
</script>
接下来,v-model不用在html标签上,而是用在组件标签上,我们来看看:
<template><!--组件上使用v-model指令<CustomInput v-model="username" />--><!--组件上使用v-model的本质--><CustomInput :modelValue="userName" @update:model-value="userName = $event"/>
</template>
此时,我们接下来创建一个CustomInput组件:
<template><!--将接收的value值赋给input元素的value属性,目的是:为了呈现数据 --><!--给input元素绑定原生input事件,触发input事件时,进而触发update:model-value事件--><input type="text" :value="modelValue" @input="emit('update:model-value',(<HTMLInputElement>$event.target).value)"/>
</template><script setup lang="ts">// 接收propsdefineProps(['modelValue'])// 声明事件const emit = defineEmits(['update:model-value'])</script>
<style scoped lang="less">
input{border: 1px solid black;background-image: linear-gradient(45deg,red,yellow,green);height:30px;font-size: 20px;color:white;
}
</style>
通过以上代码就可以利用v-model实现UI组件的通信。
3.5 $attrs
$attrs是一个响应式对象,用于实现当前组件的父组件向当前组件的子组件通信(祖 传 孙)
注意:$attrs会自动排除props中申明的属性(可能认为声明过的props被子组件自己消费了)
父组件:
<template><div class="father"><h3>父组件</h3><Child :a="a" :b="b" :c="c" :d="d" v-bind="{x:100,y:200}" :updateA="updateA"/></div>
</template><script setup lang="ts" name="Father">import Child from './Child.vue'import { ref } from "vue";let a = ref(1)let b = ref(2)let c = ref(3)let d = ref(4)function updateA(value){a.value = value}
</script>
子组件:
<template><div class="child"><h3>子组件</h3><GrandChild v-bind="$attrs"/></div>
</template><script setup lang="ts" name="Child">import GrandChild from './GrandChild.vue'
</script>
孙组件:
<template><div class="grand-child"><h3>孙组件</h3><h4>a:{{ a }}</h4><h4>b:{{ b }}</h4><h4>c:{{ c }}</h4><h4>d:{{ d }}</h4><h4>x:{{ x }}</h4><h4>y:{{ y }}</h4><button @click="updateA(666)">点我更新A</button></div>
</template><script setup lang="ts" name="GrandChild">defineProps(['a','b','c','d','x','y','updateA'])
</script>
3.6 $refs
、$parent
作用:$refs
用于父传子,$parent
用于子传父。
原理:
属性 | 说明 |
---|---|
$refs | 值为对象,包含所有被ref属性标识的DOM元素或组件实例 |
$parent | 值为对象,当前组件的父组件实例对象 |
我们准备了三个组件,一个父组件,两个字组件:
<template><!-- 这是父组件 --><div class="parent"><h2>父组件</h2><p>房产:{{ house }}</p><button @click="changeToy">修改child1的玩具</button><button @click="changeSchool">修改child2的学校</button><button @click="addBooks($refs)">给所有的子组件书籍 + 3</button><Child1 ref="c1"/><Child2 ref="c2"/></div>
</template><script setup lang="ts">
import Child1 from './Child1.vue';
import Child2 from './Child2.vue';
import {ref} from 'vue'
let house = ref(4);
let c1 = ref();
let c2 = ref();function changeToy(){c1.value.toy = "小猪佩奇"
}
function changeSchool(){c2.value.school = "初中"
}
function addBooks(refs:any){for( let key in refs){refs[key].books += 3}
}defineExpose({house})
</script><style lang="less">
.parent{width:500px;height: 500px;background-color: gray;
}
</style>
<template><div class="child1"><h2>子组件1</h2><p>玩具:{{ toy }}</p><p>书籍:{{ books }}</p><button @click="reduceHouse($parent)">卖掉父组件的一套房产</button></div>
</template><script setup lang="ts">import {ref} from 'vue'let toy =ref("奥特曼")let books = ref(4)function reduceHouse(parent:any){parent.house -= 1;}defineExpose({toy,books})
</script>
<style lang="less">
.child1{width:500px;height: 200px;background-color: green;
}
</style>
<template><div class="child2"><h2>子组件2</h2><p>学校:{{ school }}</p><p>书籍:{{ books }}</p></div></template><script setup lang="ts">import {ref} from 'vue'let school =ref("小学")let books = ref(10)defineExpose({school,books})</script><style lang="less">
.child1{width:500px;height: 200px;background-color: yellow;
}
</style>
注意:这里父子组件进行通信,需要使用defineExpose() 对外暴露相应数据或者方法。
3.7 provide、inject
作用:实现祖孙组件直接通信。
具体使用:
- 在祖先组件中通过provide配置向后代组件提供数据。
- 在后代组件中通过inject配置来申明接收数据。
<template><div class="father"><h3>父组件</h3><h4>资产:{{ money }}</h4><h4>汽车:{{ car }}</h4><button @click="money += 1">资产+1</button><button @click="car.price += 1">汽车价格+1</button><Child/></div>
</template><script setup lang="ts" name="Father">import Child from './Child.vue'import { ref,reactive,provide } from "vue";// 数据let money = ref(100)let car = reactive({brand:'奔驰',price:100})// 用于更新money的方法function updateMoney(value:number){money.value += value}// 提供数据provide('moneyContext',{money,updateMoney})provide('car',car)
</script>
<template><div class="grand-child"><h3>我是孙组件</h3><h4>资产:{{ money }}</h4><h4>汽车:{{ car }}</h4><button @click="updateMoney(6)">点我</button></div>
</template><script setup lang="ts" name="GrandChild">import { inject } from 'vue';// 注入数据let {money,updateMoney} = inject('moneyContext',{money:0,updateMoney:(x:number)=>{}})let car = inject('car')
</script>
子组件中不用编写任何东西,是不受到任何打扰的
3.8 pinia
参考上一长文章pinia讲解。
3.9 slot
slot插槽的详细讲解 在slot插槽的用法 文章中可以查看
3.9.1 默认插槽
父组件中:<Category title="今日热门游戏"><ul><li v-for="g in games" :key="g.id">{{ g.name }}</li></ul></Category>
子组件中:<template><div class="item"><h3>{{ title }}</h3><!-- 默认插槽 --><slot></slot></div></template>
3.9.2 具名插槽
父组件中:<Category title="今日热门游戏"><template v-slot:s1><ul><li v-for="g in games" :key="g.id">{{ g.name }}</li></ul></template><template #s2><a href="">更多</a></template></Category>
子组件中:<template><div class="item"><h3>{{ title }}</h3><slot name="s1"></slot><slot name="s2"></slot></div></template>
3.9.3 作用于插槽
父组件中:<Game v-slot="params"><!-- <Game v-slot:default="params"> --><!-- <Game #default="params"> --><ul><li v-for="g in params.games" :key="g.id">{{ g.name }}</li></ul></Game>子组件中:<template><div class="category"><h2>今日游戏榜单</h2><slot :games="games" a="哈哈"></slot></div></template><script setup lang="ts" name="Category">import {reactive} from 'vue'let games = reactive([{id:'asgdytsa01',name:'英雄联盟'},{id:'asgdytsa02',name:'王者荣耀'},{id:'asgdytsa03',name:'红色警戒'},{id:'asgdytsa04',name:'斗罗大陆'}])</script>
以上就是Vue3组件常用通信方式,如果觉得本篇文章对您有用,还请一键三连,非常感谢!