vue3之组件通信
Vue3
组件通信和 Vue2
的区别:
- 移出事件总线,使用
mitt
代替。 vuex
换成了pinia
。- 把
.sync
优化到了v-model
里面了。 - 把
$listeners
所有的东西,合并到$attrs
中了。 $children
被砍掉了。
常见搭配形式:
1、props 使用
概述:props
是使用频率最高的一种通信方式,常用与 :父 ↔ 子。
- 若 父传子:属性值是非函数。
- 若 子传父:属性值是函数。
父组件: Father.vue
<template>
<div class="father">
<h3>父组件</h3>
<h4>汽车:{{ car }}</h4>
<h4 v-show="toy">子给的玩具:{{ toy }}</h4>
<Child :car="car" :sendToy="getToy" />
</div>
</template>
<script setup lang="ts" name="Father">
import Child from './Child.vue'
import { ref } from 'vue'
let car = ref('奔驰')
let toy = ref('')
function getToy(value: string) {
toy.value = value
}
</script>
<style scoped>
.father {
background-color: rgb(165, 164, 164);
padding: 20px;
border-radius: 10px;
}
</style>
子组件: Child.vue
<template>
<div class="child">
<h3>子组件</h3>
<h4>玩具:{{ toy }}</h4>
<h4>父给的车:{{ car }}</h4>
<button @click="sendToy(toy)">把玩具给父亲</button>
</div>
</template>
<script setup lang="ts" name="Child">
import { ref } from 'vue'
let toy = ref('奥特曼')
// 声明接收 props
defineProps(['car', 'sendToy'])
</script>
<style scoped>
.child {
background-color: skyblue;
padding: 10px;
box-shadow: 0 0 10px black;
border-radius: 10px;
}
</style>
测试结果:
2、自定义事件
- 概述:自定义事件常用于:子 => 父。
- 注意区分好:原生事件、自定义事件。
- 原生事件:
- 事件名是特定的(
click
、mosueenter
等等) - 事件对象
$event
: 是包含事件相关信息的对象(pageX
、pageY
、target
、keyCode
)
- 事件名是特定的(
- 自定义事件:
- 事件名是任意名称
- 事件对象
$event
: 是调用emit
时所提供的数据,可以是任意类型!!!
父组件: Father.vue
<template>
<div class="father">
<h3>父组件</h3>
<h4 v-show="toy">子给的玩具:{{ toy }}</h4>
<button @click="test(1, $event)">点我</button>
<!-- 给子组件 Child 绑定自定义事件;多个单词官方推荐用肉串命名规则 -->
<Child @send-toy="saveToy" />
</div>
</template>
<script setup lang="ts" name="Father">
import Child from './Child.vue'
import { ref } from "vue";
let toy = ref('')
function saveToy(value: string) {
console.log('saveToy', value)
toy.value = value
}
function test(value: number, e: Event) {
console.log(e) // e 为 PointerEvent 事件对象
}
</script>
<style scoped>
.father {
background-color: rgb(165, 164, 164);
padding: 20px;
border-radius: 10px;
}
.father button {
margin-right: 5px;
}
</style>
子组件: Child.vue
<template>
<div class="child">
<h3>子组件</h3>
<h4>玩具:{{ toy }}</h4>
<!-- 2.触发 send-toy 事件 -->
<button @click="emit('send-toy', toy)">把玩具给父组件</button>
</div>
</template>
<script setup lang="ts" name="Child">
import { ref } from "vue";
let toy = ref('奥特曼')
// 1.声明事件
const emit = defineEmits(['send-toy'])
</script>
<style scoped>
.child {
margin-top: 10px;
background-color: rgb(76, 209, 76);
padding: 10px;
box-shadow: 0 0 10px black;
border-radius: 10px;
}
</style>
测试结果:
3、mitt 使用
概述:与消息订阅与发布(pubsub
)功能类似,可以实现任意组件间通信。
安装mitt
npm i mitt
引入 emitter: /src/utils/emitter.ts
// 引入 mitt
import mitt from 'mitt'
// 调用 mitt 得到 emitter,emitter能:绑定事件、触发事件
const emitter = mitt()
// 绑定事件
// emitter.on('test1', () => {
// console.log('test1被调用了')
// })
// 触发事件
// setInterval(() => {
// emitter.emit('test1')
// }, 1000);
// setTimeout(() => {
// emitter.off('test1')
// emitter.all.clear()
// }, 3000);
// 暴露 emitter
export default emitter
父组件: Father.vue
<template>
<div class="father">
<h3>父组件</h3>
<Child1/>
<Child2/>
</div>
</template>
<script setup lang="ts" name="Father">
import Child1 from './Child1.vue'
import Child2 from './Child2.vue'
</script>
<style scoped>
.father{
background-color:rgb(165, 164, 164);
padding: 20px;
border-radius: 10px;
}
.father button{
margin-left: 5px;
}
</style>
子组件1: Child1.vue
<template>
<div class="child1">
<h3>子组件1</h3>
<h4>玩具:{{ toy }}</h4>
<!-- 触发 -->
<button @click="emitter.emit('send-toy', toy)">玩具给弟弟(Child2)</button>
</div>
</template>
<script setup lang="ts" name="Child1">
import { ref } from 'vue'
import emitter from '@/utils/emitter';
let toy = ref('奥特曼')
</script>
<style scoped>
.child1 {
margin-top: 50px;
background-color: skyblue;
padding: 10px;
box-shadow: 0 0 10px black;
border-radius: 10px;
}
.child1 button {
margin-right: 10px;
}
</style>
子组件2: Child1.vue
<template>
<div class="child2">
<h3>子组件2</h3>
<h4>电脑:{{ computer }}</h4>
<h4>哥哥给的玩具:{{ toy }}</h4>
</div>
</template>
<script setup lang="ts" name="Child2">
import { ref, onUnmounted } from 'vue'
import emitter from '@/utils/emitter';
let computer = ref('联想')
let toy = ref('')
// 监听:给 emitter 绑定 send-toy 事件
emitter.on('send-toy', (value: any) => {
toy.value = value
})
// 在组件卸载时解绑 send-toy 事件
onUnmounted(() => {
emitter.off('send-toy')
})
</script>
<style scoped>
.child2 {
margin-top: 50px;
background-color: orange;
padding: 10px;
box-shadow: 0 0 10px black;
border-radius: 10px;
}
</style>
测试结果:
4、v-model 使用
概述:实现 父↔子 之间相互通信。
前序知识 ——
v-model
的本质
<!-- 使用 v-model 指令 -->
<input type="text" v-model="username">
<!-- v-model的本质是下面这行代码 -->
<input type="text" :value="username" @input="username = (<HTMLInputElement>$event.target).value">
- 组件标签上的
v-model
的本质::moldeValue
+update:modelValue
事件。
<!-- 组件标签上使用 v-model 指令 -->
<JasonInput v-model="username"/>
<!-- 组件标签上 v-model 的本质 -->
<JasonInput :modelValue="username" @update:modelValue="username = $event"/>
JasonInput
组件
<template>
<input type="text" :value="modelValue" @input="emit('update:modelValue', (<HTMLInputElement>$event.target).value)">
</template>
<script setup lang="ts" name="JasonInput">
defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>
<style scoped>
input {
border: 2px solid black;
background-image: linear-gradient(45deg, red, yellow, green);
height: 30px;
font-size: 20px;
color: white;
}
</style>
4、也可以更换 value
,例如改成 ming
和 mima
# Father.vue
<JasonInput v-model:ming="username" v-model:mima="password" />
# JasonInput 组件
```json
<template>
<input type="text" :value="ming" @input="emit('update:ming', (<HTMLInputElement>$event.target).value)">
<br>
<input type="text" :value="mima" @input="emit('update:mima', (<HTMLInputElement>$event.target).value)">
</template>
<script setup lang="ts" name="JasonInput">
defineProps(['ming', 'mima'])
const emit = defineEmits(['update:ming', 'update:mima'])
</script>
<style scoped>
input {
border: 2px solid black;
background-image: linear-gradient(45deg, red, yellow, green);
height: 30px;
font-size: 20px;
color: white;
}
</style>
测试结果:
5、$attrs 使用
- 概述:
$attrs
用于实现 当前组件的父组件 向 当前组件的子组件 通信(祖→孙)。 - 具体说明:
$attrs
是一个对象,包含所有父组件传入的标签属性。注意:
$attrs
会自动排除props
中声明的属性(可以认为声明过的props
被子组件自己“消费”了)
父组件:
<template>
<div class="father">
<h3>父组件</h3>
<h4>a:{{ a }}</h4>
<h4>b:{{ b }}</h4>
<Child :a="a" :b="b" 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)
function updateA(value: number) {
a.value += value
}
</script>
<style scoped>
.father {
background-color: rgb(165, 164, 164);
padding: 20px;
border-radius: 10px;
}
</style>
子组件:
<template>
<div class="child">
<h3>子组件</h3>
<GrandChild v-bind="$attrs"/>
</div>
</template>
<script setup lang="ts" name="Child">
import GrandChild from './GrandChild.vue'
</script>
<style scoped>
.child{
margin-top: 20px;
background-color: skyblue;
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 10px black;
}
</style>
孙组件:
<template>
<div class="grand-child">
<h3>孙组件</h3>
<h4>a:{{ a }}</h4>
<h4>b:{{ b }}</h4>
<h4>x:{{ x }}</h4>
<h4>y:{{ y }}</h4>
<button @click="updateA(6)">点我将爷爷那的a更新</button>
</div>
</template>
<script setup lang="ts" name="GrandChild">
defineProps(['a', 'b', 'x', 'y', 'updateA'])
</script>
<style scoped>
.grand-child {
margin-top: 20px;
background-color: orange;
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 10px black;
}
</style>
测试结果:
6、$$refs 和 $parent 使用
概述:
$refs
用于 :父→子。$parent
用于:子→父。
原理如下:
属性 说明 $refs
值为对象,包含所有被 ref
属性标识的DOM
元素或组件实例。$parent
值为对象,当前组件的父组件实例对象。
父组件:
<template>
<div class="father">
<h3>父组件</h3>
<h4>房产:{{ house }}</h4>
<button @click="changeToy">修改Child1的玩具</button>
<button @click="changeComputer">修改Child2的电脑</button>
<button @click="getAllChild($refs)">让所有孩子的书变多</button>
<Child1 ref="c1" />
<Child2 ref="c2" />
</div>
</template>
<script setup lang="ts" name="Father">
import Child1 from './Child1.vue'
import Child2 from './Child2.vue'
import { ref, reactive } from "vue";
let house = ref(4)
// 修改子组件1
function changeToy() {
c1.value.toy = '小猪佩奇'
}
// 修改子组件2
function changeComputer() {
c2.value.computer = '华为'
}
function getAllChild(refs: { [key: string]: any }) { // 实在不行就是直接 refs: any
console.log(refs)
for (let key in refs) {
refs[key].book += 3
}
}
// 向外部提供数据
defineExpose({ house })
</script>
<style scoped>
.father {
background-color: rgb(165, 164, 164);
padding: 20px;
border-radius: 10px;
}
.father button {
margin-bottom: 10px;
margin-left: 10px;
}
</style>
子组件1:
<template>
<div class="child1">
<h3>子组件1</h3>
<h4>玩具:{{ toy }}</h4>
<h4>书籍:{{ book }} 本</h4>
<button @click="minusHouse($parent)">干掉父亲的一套房产</button>
</div>
</template>
<script setup lang="ts" name="Child1">
import { ref } from "vue";
let toy = ref('奥特曼')
let book = ref(3)
// 修改父组件数据
function minusHouse(parent: any) {
parent.house -= 1
}
// 把数据交给外部
defineExpose({ toy, book })
</script>
<style scoped>
.child1 {
margin-top: 20px;
background-color: skyblue;
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 10px black;
}
</style>
子组件2:
<template>
<div class="child2">
<h3>子组件2</h3>
<h4>电脑:{{ computer }}</h4>
<h4>书籍:{{ book }} 本</h4>
</div>
</template>
<script setup lang="ts" name="Child2">
import { ref } from "vue";
let computer = ref('联想')
let book = ref(6)
// 把数据交给外部
defineExpose({ computer, book })
</script>
<style scoped>
.child2 {
margin-top: 20px;
background-color: orange;
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 10px black;
}
</style>
测试结果:
7、provide 和 inject 使用
概述:实现祖孙组件直接通信
具体使用:
- 在祖先组件中通过
provide
配置向后代组件提供数据 - 在后代组件中通过
inject
配置来声明接收数据
- 在祖先组件中通过
父组件:
【第一步】父组件中,使用provide
提供数据
<template>
<div class="father">
<h3>父组件</h3>
<h4>银子:{{ money }}万元</h4>
<h4>车子:一辆{{ car.brand }}车,价值{{ car.price }}万元</h4>
<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
})
function updateMoney(value: number) {
money.value -= value
}
// 向后代提供数据
provide('moneyContext', { money, updateMoney }) // 金额上下文
provide('car', car)
</script>
<style scoped>
.father {
background-color: rgb(165, 164, 164);
padding: 20px;
border-radius: 10px;
}
</style>
子组件:整个组件通信过程都不会修改
<template>
<div class="child">
<h3>我是子组件</h3>
<GrandChild />
</div>
</template>
<script setup lang="ts" name="Child">
import GrandChild from './GrandChild.vue'
</script>
<style scoped>
.child {
margin-top: 20px;
background-color: skyblue;
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 10px black;
}
</style>
孙组件:
【第二步】孙组件中使用 inject
配置项接受数据。
<template>
<div class="grand-child">
<h3>我是孙组件</h3>
<h4>银子:{{ money }}</h4>
<h4>车子:一辆{{ car.brand }}车,价值{{ car.price }}万元</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: (n: number) => { } })
let car = inject('car', { brand: '未知', price: 0 })
</script>
<style scoped>
.grand-child {
background-color: orange;
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 10px black;
}
</style>
8、pinia
参考之前 pinia
部分的讲解
9、slot 插槽的使用
9.1 默认插槽
Category.vue
组件
<template>
<div class="category">
<h2>{{ title }}</h2>
<!-- 默认插槽 -->
<slot></slot>
</div>
</template>
<script setup lang="ts" name="Category">
defineProps(['title'])
</script>
<style scoped>
.category {
background-color: skyblue;
border-radius: 10px;
box-shadow: 0 0 10px;
padding: 10px;
width: 200px;
height: 300px;
}
h2 {
background-color: orange;
text-align: center;
font-size: 20px;
font-weight: 800;
}
</style>
Father.vue
组件
<template>
<div class="father">
<h3>父组件</h3>
<div class="content">
<Category title="热门游戏列表">
<ul>
<li v-for="g in games" :key="g.id">{{ g.name }}</li>
</ul>
</Category>
<Category title="今日美食城市">
<img :src="imgUrl" alt="">
</Category>
<Category title="今日影视推荐">
<video :src="videoUrl" controls></video>
</Category>
</div>
</div>
</template>
<script setup lang="ts" name="Father">
import Category from './Category.vue'
import { ref, reactive } from "vue";
let games = reactive([
{ id: 'asgytdfats01', name: '英雄联盟' },
{ id: 'asgytdfats02', name: '王者农药' },
{ id: 'asgytdfats03', name: '红色警戒' },
{ id: 'asgytdfats04', name: '斗罗大陆' }
])
let imgUrl = ref('https://z1.ax1x.com/2023/11/19/piNxLo4.jpg')
let videoUrl = ref('http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4')
</script>
<style scoped>
.father {
background-color: rgb(165, 164, 164);
padding: 20px;
border-radius: 10px;
}
.content {
display: flex;
justify-content: space-evenly;
}
img,
video {
width: 100%;
}
</style>
测试结果:
9.2 具名插槽
Category.vue
组件
<template>
<div class="category">
<!-- s1 插槽 -->
<slot name="s1"></slot>
<!-- s2 插槽 -->
<slot name="s2"></slot>
</div>
</template>
<script setup lang="ts" name="Category">
</script>
<style scoped>
.category {
background-color: skyblue;
border-radius: 10px;
box-shadow: 0 0 10px;
padding: 10px;
width: 200px;
height: 300px;
}
</style>
Father.vue
组件
<template>
<div class="father">
<h3>父组件</h3>
<div class="content">
<Category>
<!-- 指定使用 s2 插槽 -->
<template v-slot:s2>
<ul>
<li v-for="g in games" :key="g.id">{{ g.name }}</li>
</ul>
</template>
<template v-slot:s1>
<h2>热门游戏列表</h2>
</template>
</Category>
<Category>
<!-- 使用插槽时不会因为放置顺序不同而展示位置发送变化,而是根据组件中定义的插槽顺序展示 -->
<template v-slot:s2>
<img :src="imgUrl" alt="">
</template>
<template v-slot:s1>
<h2>今日美食城市</h2>
</template>
</Category>
<Category>
<!-- 具名插槽简写格式 -->
<template #s2>
<video video :src="videoUrl" controls></video>
</template>
<template #s1>
<h2>今日影视推荐</h2>
</template>
</Category>
</div>
</div>
</template>
<script setup lang="ts" name="Father">
import Category from './Category.vue'
import { ref, reactive } from "vue";
let games = reactive([
{ id: 'asgytdfats01', name: '英雄联盟' },
{ id: 'asgytdfats02', name: '王者农药' },
{ id: 'asgytdfats03', name: '红色警戒' },
{ id: 'asgytdfats04', name: '斗罗大陆' }
])
let imgUrl = ref('https://z1.ax1x.com/2023/11/19/piNxLo4.jpg')
let videoUrl = ref('http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4')
</script>
<style scoped>
.father {
background-color: rgb(165, 164, 164);
padding: 20px;
border-radius: 10px;
}
.content {
display: flex;
justify-content: space-evenly;
}
img,
video {
width: 100%;
}
h2 {
background-color: orange;
text-align: center;
font-size: 20px;
font-weight: 800;
}
</style>
测试结果:
9.3 作用域插槽
理解:数据在组件的自身,但根据数据生成的结构需要组件的使用者来决定。(新闻数据在 News
组件中,但使用数据所遍历出来的结构由 App
组件决定)
Game.vue
组件
<template>
<div class="game">
<h2>游戏列表</h2>
<slot :youxi="games" x="哈哈" y="你好"></slot>
</div>
</template>
<script setup lang="ts" name="Game">
import { reactive } from 'vue'
let games = reactive([
{ id: 'asgytdfats01', name: '英雄联盟' },
{ id: 'asgytdfats02', name: '王者农药' },
{ id: 'asgytdfats03', name: '红色警戒' },
{ id: 'asgytdfats04', name: '斗罗大陆' }
])
</script>
<style scoped>
.game {
width: 200px;
height: 300px;
background-color: skyblue;
border-radius: 10px;
box-shadow: 0 0 10px;
}
h2 {
background-color: orange;
text-align: center;
font-size: 20px;
font-weight: 800;
}
</style>
Father.vue
组件
<template>
<div class="father">
<h3>父组件</h3>
<div class="content">
<Game>
<!-- params 是插槽传递的所有数据对象 -->
<template v-slot="params">
<!-- ul->li -->
<ul>
<li v-for="y in params.youxi" :key="y.id">
{{ y.name }}
</li>
</ul>
</template>
</Game>
<Game>
<template v-slot="params">
<!-- ol-li -->
<ol>
<li v-for="item in params.youxi" :key="item.id">
{{ item.name }}
</li>
</ol>
</template>
</Game>
<Game>
<!-- 直接从 params 中结构出 youxi -->
<!-- 作用域插槽也可以指定名字,默认为:default -->
<template #default="{ youxi }">
<!-- h3 -->
<h3 v-for="g in youxi" :key="g.id">{{ g.name }}</h3>
</template>
</Game>
</div>
</div>
</template>
<script setup lang="ts" name="Father">
import Game from './Game.vue'
</script>
<style scoped>
.father {
background-color: rgb(165, 164, 164);
padding: 20px;
border-radius: 10px;
}
.content {
display: flex;
justify-content: space-evenly;
}
img,
video {
width: 100%;
}
</style>
测试结果: