【Vue3】组件通信
在开发中,组件之间的通信是很重要的,组件间通信的方法也有很多。父组件传参给子组件、子组件传参给父组件、祖组件传参给孙组件、孙组件传参给祖组件、兄弟组件以及任意组件间的传递,在本文中将一一记录。
1. 【父传子】props通信
父组件通过props向子组件传参是通过在子组件的标签上通过属性绑定的方式传递参数(传递的参数不能是函数)
<!-- 父组件 -->
<template>
<div>
<!-- 子组件 -->
<!-- 在子组件上自定义属性name就能传给子组件了 -->
<Child :name='name' />
</div>
</template>
<script lang='ts' setup name='Father'>
import Child from './Child.vue'
import { ref } from 'vue'
// 创建一个想要传给子组件的参数
let name = ref('Nimo')
</script>
<!-- 子组件 -->
<template>
<div>
<h1>名字:{{ name }}</h1>
</div>
</template>
<script lang='ts' setup name='Child'>
// 子组件需要使用definePorps接收参数
defineProps(['name'])
</script>
2.【父传子】V-model
父组件可以通过在自组建的标签内使用 v-model:params 的方式向子组件传递数据,子组件需要用defineProps接收参数。
<!-- 父组件 -->
<template>
<div>
<!-- 子组件 -->
<!-- 在子组件上自定义属性name就能传给子组件了 -->
<Child v-model:name='name' />
</div>
</template>
<script lang='ts' setup name='Father'>
import Child from './Child.vue'
import { ref } from 'vue'
// 创建一个想要传给子组件的参数
let name = ref('Nimo')
</script>
<!-- 子组件 -->
<template>
<div>
<h1>名字:{{ name }}</h1>
</div>
</template>
<script lang='ts' setup name='Child'>
// 子组件需要使用definePorps接收参数
defineProps(['name'])
</script>
3.【父传子】$refs传参
在子组件上绑定ref属性,子组件再通过defineExpose()将数据暴露出去。就可以对子组件进行传值
<!-- 父组件 -->
<template>
<div>
<!-- 子组件 -->
<!-- 在子组件上绑定ref -->
<Child ref='c1' />
<button @click='modifyName'>修改子组件的名称</button>
</div>
</template>
<script lang='ts' setup name='Father'>
import Child from './Child.vue'
import { ref } from 'vue'
function modifyName(){
c1.value.name = 'Kiko'
}
</script>
<!-- 子组件 -->
<template>
<div>
<h1>名字:{{ name }}</h1>
</div>
</template>
<script lang='ts' setup name='Child'>
import { ref } from 'vue'
let name = ref('Nimo')
// 子组件需要使用defineExpose将数据暴露出去,父组件才能访问到
defineExpose({ name })
</script>
4.【父传子】默认插槽、具名插槽
在父组件中的子组件必须使用双标签,标签内的内容可以传到子组件<slot>标签所在的位置。
- 默认插槽
<!-- 父组件 -->
<template>
<div>
<!-- 子组件 -->
<Child>
<!-- 包裹在Child标签内的东西就会出现在Child组件中的slot标签所在的位置 -->
<h1> {{ name }} </h1>
</Child>
</div>
</template>
<script lang='ts' setup name='Father'>
import Child from './Child.vue'
import { ref } from 'vue'
// 创建变量name
let name = ref('Nimo')
</script>
<!-- 子组件 -->
<template>
<div>
<!-- 默认插槽 -->
<slot></slot>
</div>
</template>
<script lang='ts' setup name='Child'>
import { ref } from 'vue'
</script>
- 具名插槽:当slot标签有name属性的时候,父组件必须使用v-slot包裹内容才行
<!-- 父组件 -->
<template>
<div>
<!-- 子组件 -->
<Child>
<!-- 由于子组件是具名插槽,父组件要传给子组件来替代slot的内容必须使用v-slot标注 -->
<template v-slot:s1>
<h1> {{ name }} </h1>
</template>
</Child>
<Child>
<!-- v-slot:插槽名 有一个语法糖:#name -->
<!-- 使用 #插槽名 的方式标注和使用v-slot:插槽名的形式效果一致 -->
<template #s1>
<h1> {{ name }} </h1>
</template>
</Child>
</div>
</template>
<script lang='ts' setup name='Father'>
import Child from './Child.vue'
import { ref } from 'vue'
// 创建变量name
let name = ref('Nimo')
</script>
<!-- 子组件 -->
<template>
<div>
<!-- 具名插槽 -->
<slot name='s1'></slot>
</div>
</template>
<script lang='ts' setup name='Child'>
import { ref } from 'vue'
</script>
5. 【子传父】props传参
在使用props进行父子组件通信时,如果父组件想要接收从子组件传来的信息,需要通过父组件监听子组件的事件完成数据的接收。
<!-- 父组件 -->
<template>
<div>
<div class="outer">
<h3>父组件</h3>
<!-- :sendToy为子组件的事件名,getToy为父组件监听子组件事件触发的事件 -->
<Child :sendToy="getToy"></Child>
</div>
</div>
</template>
<script setup lang="ts" name="father">
import Child from './Child.vue'
import {ref} from 'vue'
/**
* 1.通过props进行组件通信:
* - 父传子:在子组件的标签上通过属性绑定的方式传递数据,传递的参数不是函数
* - 子传父:子组件通过函数发送数据给父组件,父组件通过一个函数接受子组件传来的值
*
* tips: 如果组件中嵌套组件,不推荐使用props进行通信
*
*/
// 方法
function getToy(value:string){
console.log('父组件的方法',value);
}
</script>
<!- 子组件 -->
<template>
<div>
<div class="inner">
<h4>子组件</h4>
<button @click="sendToy(toy)">把玩具给父亲</button>
</div>
</div>
</template>
<script setup lang="ts" name="child">
import { ref } from "vue";
let toy = ref("小熊");
// 声明接受props
let props = defineProps(["car", "sendToy"]);
</script>
6. 【子传父】自定义事件
要通过自定义事件的方式实现子组件传数据给父组件需要使用mitt
- 安装mitt
安装命令:npm install mitt
- 配置emitter.ts,引入mitt
创建如下文件 src/utils/emitter.ts
// 引入mitt
import mitt from 'mitt'
// 调用mitt()
// emitter能绑定事件,触发事件,解绑事件
const emitter = mitt()
//导出emitter
export default emitter
- 在vue中使用mitt
<!-- 父组件 -->
<template>
<div>
<div class="outer">
<h3>父组件</h3>
<h2>子组件发来的信息:{{ msg }}</h2>
<Child1></Child1>
</div>
</div>
</template>
<script setup lang="ts" name="father">
import Child1 from './Child1.vue'
import {ref, onUnmounted} from 'vue'
import emitter from '@/utils/emitter'
/**
* mitt:
* - 安装mitt:npm i mitt
* - 配置emitter.ts配置,引入mitt
* - 在emitter.ts中创建emitter对象
* - 在组件中引入emitter.ts,使用emitter.on()绑定事件,使用emitter.emit()触发事件
* tips: 在组件中最后解绑事件 emitter.off()
*/
let msg = ref('a default message')
// 1.给emitter绑定send-message事件
emitter.on('send-message',(val:any) => {
msg.value = val;
})
onUnmounted(() => {
// 3.组件销毁后解绑事件
emitter.off('send-message');
})
</script>
<!-- 子组件 -->
<template>
<div>
<div class="inner">
<h4>子组件</h4>
<h5>信息:{{ msg }}</h5>
<!-- 2.通过emitter.emit()发送自定义事件和数据 -->
<button @click="emitter.emit('send-message',msg)">把信息发给父组件</button>
</div>
</div>
</template>
<script setup lang="ts" name="child1">
import {ref,onUnmounted} from 'vue'
import emitter from '@/utils/emitter'
let msg = ref('this is a message');
</script>
7. 【子传父】$parent通信
子组件可以使用$parent获取到父组件暴露出来的数据。
<!-- 父组件 -->
<template>
<div>
<div class="outer">
<h3>父组件</h3>
<h2>子组件发来的信息:{{ msg }}</h2>
<Child></Child1>
</div>
</div>
</template>
<script setup lang="ts" name="father">
/**
* $refs: 父传子
* $parent: 子传父
*/
import Child from "./Child.vue";
import { ref } from "vue";
let msg = ref('this is a default message');
// 把数据暴露出去!!!不暴露数据就无法获取到父组件的数据
defineExpose({ msg });
</script>
<!-- 子组件 -->
<template>
<div class="inner">
<h4>子组件</h4>
<!-- 通过$parent获取父组件 -->
<button @click="sendMessage($parent)">给父组件发送信息</button>
</div>
</template>
<script setup lang="ts" name="child">
import { ref } from "vue";
// 方法
function sendMessage(parent: any) {
parent.msg = "this is a message sent by Child";
}
</script>
8. 【子传父】作用域插槽
使用作用域插槽也可实现子传父形式的组件通信。
与之前介绍过的插槽几乎是一样的,在子组件中通过放置<slot>标签进行占位,在父组件中,子组件双标签内的结构会替换到子组件的<slot>标签所在的位置。但是毕竟父子组件是两个文件,如何能让父组件接收到出现在子组件中的数据呢?
子组件通过在<slot>标签上绑定属性,父组件就能获取到子组件中的值。
<!-- 子组件 -->
<template>
<div class="game">
<h2>游戏列表</h2>
<!-- 通过在slot标签上的games属性绑定gamesList -->
<slot name="s1" :games="gamesList"></slot>
</div>
</template>
<script setup lang='ts' name='game'>
import {reactive} from 'vue'
// 创建一些数据,希望父组件能获取到
let gamesList = reactive([
{ id: "bwviuhnwiue", name: "王者荣耀" },
{ id: "nfwiehvowe", name: "阴阳师" },
{ id: "vnoiwjiuhwiue", name: "红色警戒" },
{ id: "bfcuwbiwie", name: "斗罗大陆" },
]);
</script>
<!-- 父组件 -->
<template>
<div class="outer">
<h3>父组件</h3>
<div class="wrap">
<Game>
<!-- 作用域插槽可以接收子组件传来的参数 -->
<!-- 使用 v-slot="xxx" 接收参数 -->
<template v-slot:s1="params">
<ul>
<li v-for="item in params.games" :key="item.id">{{ item.name }}</li>
</ul>
</template>
</Game>
<Game>
<!-- 作用域插槽也可具名 语法:v-slot:name="params" -->
<!-- 语法糖:#name="params",把 params 解构后就是如下的形式 -->
<template #s1="{ games }">
<h3 v-for="item in games" :key="item.id">{{ item.name }}</h3>
</template>
</Game>
</div>
</div>
</template>
<script setup lang="ts" name="father">
import Game from "./Game.vue";
</script>
9.【祖孙互传】$attrs 通信
在项目中往往可能出现组件嵌套的情况,祖先组件需要和子孙组件之间互相通信。当然可以使用上述的父传子和子传父的通信方式将信息一层层传递,但是这样的方法会使得组件之间的耦合度过高,所以就是,别这么干。想要实现祖孙之间的通信,也有好的方法。
$attrs 可以实现祖孙组件之间的通信。
<!-- 父组件 -->
<template>
<div class="outer">
<h3>父组件</h3>
<Child
:a="a"
:b="b"
:c="c"
:d="d"
v-bind="{ x: 100, y: 200 }"
:updateA="getUpdate"
/>
</div>
</template>
<script setup lang="ts" name="father">
import { ref } from "vue";
import Child from "./Child.vue";
let a = ref(1);
let b = ref(2);
let c = ref(3);
let d = ref(4);
</script>
<!-- 子组件 -->
<template>
<div class="inner">
<h4>子组件</h4>
<!-- !!!必须在孙子组件上使用v-bind绑定$attrs,孙子组件才能获取到祖先组件的值 -->
<GrandChild v-bind="$attrs" />
</div>
</template>
<script setup lang="ts" name="child">
import GrandChild from "./GrandChild.vue";
</script>
<!-- 孙子组件 -->
<template>
<div class="inner">
<h4>孙组件</h4>
<h5>a: {{ a }}</h5>
<h5>b: {{ b }}</h5>
<h5>c: {{ c }}</h5>
<h5>d: {{ d }}</h5>
<!-- $attrs中的值是祖先组件中创建了的,但是没有在孙子组件中通过defineProps()获取过的数据 -->
<h5>{{ $attrs }}</h5>
</div>
</template>
<script setup lang="ts" name="grandChild">
// 通过defineProps()获取过的数据,不会出现在$attrs中
// 注意:如果在子组件中没有通过v-bind绑定 $attrs,则孙子组件也无法获取到祖先组件中的值
defineProps(["a", "b", "c", "d", "updateA"]);
</script>
10.【祖孙互传】 provide和inject通信
上面介绍了使用 $attrs 来进行祖孙之间的通信,但是你会发现,祖孙之间的通信中间仍然需要子组件在中间起到一个“牵线搭桥”的作用,那有没有什么方法可以不经过子组件,实现祖孙组件的直接通信呢?
使用provide和inject就可以实现这一目的。provide和inject从单词的意思就可以看出,provide:提供,inject:注入。
<!-- 父组件 -->
<template>
<div class="outer">
<h3>父组件</h3>
<h4>财产: {{ money }}</h4>
<h4>车子: 一辆{{ car.brand }}车,价值{{ car.price }}万元</h4>
<Child/>
</div>
</template>
<script setup lang="ts" name="father">
/**
* provide and inject: 实现祖孙组件之间的传值
* 引入 provide 和 inject
* - provide: 向后代组件提供数据
* - inject: 注入上级组件提供的数据
*
*/
import { ref,reactive,provide } from "vue";
import Child from "./Child.vue";
let money = ref(100);
let car = reactive({
brand:'BMW',
price: 100
})
function updateMoney(val){
money.value -= val;
}
// 向后代组件提供数据
provide('money',{money,updateMoney});
provide('car',car);
</script>
<!-- 子孙组件 -->
<template>
<div class="inner2">
<h4>孙组件</h4>
<h5>继承了爷爷的财产:{{ money }}</h5>
<h5>继承了爷爷的车:一辆{{ car.brand }}车,价值{{ car.price }}万元</h5>
<button @click="updateMoney(5)">花爷爷的钱</button>
</div>
</template>
<script setup lang="ts" name="grandChild">
import { inject } from "vue";
// inject('money','default') 代表如果没有找到money这个provide,那么就使用默认值default
let {money,updateMoney} = inject('money',{money:'default',updateMoney:(x:number)=>{}});
let car = inject('car',{brand:'unknown',price:0});
</script>
11. 【任意组件间传值】Pinia 状态管理
使用Pinia状态管理库可以把数据和方法存放在公共的仓库里,有需要的组件只需要引入仓库就可访问使用。具体Pinia的使用方法可以查看我的上一篇《Vue3学习记录(五)》中的内容,详细介绍了如何使用Pinia。