Vue3 --- 组件通信
1. 父向子
1. props
0. 定义数据需要实现的接口
export interface Person {
id: string
name: string;
age: number;
}
// export type PersonList = Array<Person>;
export type PersonList = Person[];
1. 父组件中, 在子组件实例标签中以参数的形式传递数据
<template>
<!-- 1. 使用 :变量名 给子组件传递非字符串类型的数据 -->
<Person :personList="personList"/>
</template>
<script setup lang="ts" name="Father">
import Person from "@/components/Person.vue";
import {reactive} from "vue";
import {type PersonList} from '@/types'
let personList = reactive<PersonList>([
{id: "asdfasdgasd01", name: "小明", age: 20},
{id: "asdfasdgasd02", name: "小红", age: 15},
{id: "asdfasdgasd03", name: "小白", age: 10}
])
</script>
<style scoped>
</style>
2. 子组件中声明接收父组件传递过来的数据
0. 只接受数据
<template>
<!-- 2. 页面中使用父组件传递过来的数据 -->
<ul>
<li v-for="p in personList" :key="p.id">{{ p.id }} -- {{ p.name }} -- {{ p.age }}</li>
</ul>
</template>
<script setup lang="ts" name="Person">
// 1. 接收父组件传来的参数
defineProps(['personList'])
</script>
<style scoped>
.app {
background-color: aqua;
}
</style>
1. 读取数据
<template>
<!-- 3. 页面中使用父组件传递过来的数据 -->
<ul>
<li v-for="p in personList" :key="p.id">{{ p.id }} -- {{ p.name }} -- {{ p.age }}</li>
</ul>
</template>
<script setup lang="ts" name="Person">
import {defineProps} from 'vue'; // defineProps 可以不引入, vue 内部已经缓存了这个函数
// 1. 接收父组件传来的参数, 并赋值
const props = defineProps(['personList'])
console.log(props.personList[0].id) // 2. 取值
</script>
<style scoped>
.app {
background-color: aqua;
}
</style>
2. 接收数据,并限制数据符合指定接口的类型
<template>
<!-- 3. 页面中使用父组件传递过来的数据 -->
<ul>
<li v-for="p in personList" :key="p.id">{{ p.id }} -- {{ p.name }} -- {{ p.age }}</li>
</ul>
</template>
<script setup lang="ts" name="Person">
import {defineProps} from 'vue'; // defineProps 可以不引入, vue 内部已经缓存了这个函数
import {type PersonList} from '@/types'
// 1. 接收父组件传来的参数, 并赋值, 通过 PersonList 接口来限制类型和字段
const props = defineProps<{ personList: PersonList }>()
console.log(props.personList[0].id) // 2. 取值
</script>
<style scoped>
.app {
background-color: aqua;
}
</style>
3. 接收数据, 并限制数据符合指定接口的类型, 限制必要性, 指定默认值
<template>
<!-- 3. 页面中使用父组件传递过来的数据 -->
<ul>
<li v-for="p in personList" :key="p.id">{{ p.id }} -- {{ p.name }} -- {{ p.age }}</li>
</ul>
</template>
<script setup lang="ts" name="Person">
import {defineProps} from 'vue'; // defineProps 可以不引入, vue 内部已经缓存了这个函数
import {type PersonList} from '@/types'
// 1. 接收父组件传来的参数, 并赋值, 通过 PersonList 接口来限制类型和字段
// 使用 ? 来指定非必要的限制
// 使用 withDefaults 来设置默认值
withDefaults(defineProps<{ personList?: PersonList }>(), {
personList: () => [{
id: "asdfasdgasd01",
name: "小明",
age: 20
}]
})
</script>
<style scoped>
.app {
background-color: aqua;
}
</style>
2. 通过获取子组件实例操作子组件的数据
1. ref 标识
src/components/Father.vue
<template>
<div class="app">
<h2>我是父组件</h2>
<h4>房产: {{ houseNum }}</h4>
<h4>来自儿子组件的数据: {{ defaultData }}</h4>
<button @click="changeComputer">修改 Son1 的电脑</button>
<button @click="changeToy">修改 Son2 的玩具</button>
<!-- 1. 使用 ref 来标记子组件实例 -->
<Son1 ref="son1"/>
<Son2 ref="son2"/>
</div>
</template>
<script lang="ts" setup name="App">
import Son1 from '@/components/Son1.vue';
import Son2 from '@/components/Son2.vue';
import { ref } from 'vue';
let defaultData = ref('')
let houseNum = ref(4)
// 2. 取出子组件实例
let son1 = ref()
let son2 = ref()
// 3. 想要修改儿子组件的数据, 需要儿子组件中使用 defineExpose 对外暴露数据
function changeComputer(){
son1.value.computer = "华为笔记本"
}
function changeToy(){
son2.value.toy = "游戏机"
}
</script>
<style scoped>
.app{
width: 100%;
height: 800px;
border-radius: 20px;
background-color: burlywood;
overflow: hidden;
}
</style>
src/components/Son1.vue
<template>
<div class="son">
<h2>我是儿子1组件</h2>
<h4>玩具: {{ computer }}</h4>
<h4>书: {{ book }}</h4>
</div>
</template>
<script lang="ts" setup name="Son">
import { ref } from 'vue';
const computer = ref("苹果笔记本")
let book = ref(6)
// 1. 向外暴露 toy
defineExpose({
computer
})
</script>
<style scoped>
.son{
width: 1200px;
height: 250px;
border-radius: 20px;
background-color: cadetblue;
margin: 0 auto;
margin-top: 50px;
}
</style>
src/components/Son2.vue
<template>
<div class="grandson">
<h2>我是儿子2组件</h2>
<h4>玩具: {{ toy }}</h4>
<h4>书: {{ book }}</h4>
</div>
</template>
<script lang="ts" setup name="GrandSon">
import { ref } from 'vue';
const toy = ref("奥特曼")
let book = ref(3)
// 1. 向外暴露 toy
defineExpose({
toy
})
</script>
<style scoped>
.grandson{
width: 1200px;
height: 250px;
border-radius: 20px;
background-color: brown;
margin: 0 auto;
margin-top: 50px;
}
</style>
2. $refs
src/components/Father.vue
<template>
<div class="app">
<h2>我是父组件</h2>
<h4>房产: {{ houseNum }}</h4>
<h4>来自儿子组件的数据: {{ defaultData }}</h4>
<!-- 2. 将 $refs 当做参数 传进回调函数中, $refs 就是所有的子组件实例数组 -->
<button @click="allSonBuyBook($refs)">所有孩子的数加三本</button>
<!-- 1. 使用 ref 来标记子组件实例 -->
<Son1 ref="son1"/>
<Son2 ref="son2"/>
</div>
</template>
<script lang="ts" setup name="App">
import Son1 from '@/components/Son1.vue';
import Son2 from '@/components/Son2.vue';
import { ref } from 'vue';
let defaultData = ref('')
let houseNum = ref(4)
function allSonBuyBook(refs:any){
// refs 就是所有的子组件实例
for (const key in refs) {
refs[key].book += 3
}
}
</script>
<style scoped>
.app{
width: 100%;
height: 800px;
border-radius: 20px;
background-color: burlywood;
overflow: hidden;
}
</style>
src/components/Son1.vue
<template>
<div class="son">
<h2>我是儿子1组件</h2>
<h4>玩具: {{ computer }}</h4>
<h4>书: {{ book }}</h4>
</div>
</template>
<script lang="ts" setup name="Son">
import { ref } from 'vue';
const computer = ref("苹果笔记本")
let book = ref(6)
// 1. 向外暴露 toy
defineExpose({
computer,
book
})
</script>
<style scoped>
.son{
width: 1200px;
height: 250px;
border-radius: 20px;
background-color: cadetblue;
margin: 0 auto;
margin-top: 50px;
}
</style>
src/components/Son2.vue
<template>
<div class="grandson">
<h2>我是儿子2组件</h2>
<h4>玩具: {{ toy }}</h4>
<h4>书: {{ book }}</h4>
</div>
</template>
<script lang="ts" setup name="GrandSon">
import { ref } from 'vue';
const toy = ref("奥特曼")
let book = ref(3)
// 1. 向外暴露 toy
defineExpose({
toy,
book
})
</script>
<style scoped>
.grandson{
width: 1200px;
height: 250px;
border-radius: 20px;
background-color: brown;
margin: 0 auto;
margin-top: 50px;
}
</style>
2. 子向父
1. 自定义函数
src/components/Father.vue
<template>
<div class="app">
<div>
接收到来自子组件的数据: <span>{{ toy }}</span>
</div>
<!-- 2. 将定义好的函数传递给子组件, 名字叫 sendToy -->
<Home :sendToy="getToy"/>
</div>
</template>
<script lang="ts" setup name="App">
import { ref } from 'vue';
import Home from './components/Home.vue';
let toy = ref('')
// 1. 定义用来接收子组件数据的函数
function getToy(value: string){
console.log("父组件接收到来自子组件的数据: ", value);
toy.value = value
}
</script>
<style scoped>
</style>
src/components/Son.vue
<template>
<h2>我是子组件Home</h2>
<div>
子组件的数据: <span>{{ toy }}</span>
</div>
<!-- 2. 直接调用父组件传递的 sendToy 函数, 并填写参数 -->
<button @click="sendToy(toy)">把玩具发送给父组件</button>
</template>
<script lang="ts" setup name="Home">
import { ref } from 'vue';
const toy = ref("奥特曼")
// 1. 接受来自父组件用来发送数据的方法 sendToy
defineProps(['sendToy'])
</script>
2. 自定义事件
src/components/Father.vue
<template>
<div class="app">
<div>
接收到来自子组件的数据: <span>{{ toy }}</span>
</div>
<!-- 1. 给子组件绑定一个自定义事件 -->
<Home @send-toy="saveToy"/>
</div>
</template>
<script lang="ts" setup name="App">
import { ref } from 'vue';
import Home from './components/Home.vue';
let toy = ref('')
// 2. 定义自定义事件的回调函数, 并接收子组件传递的参数
function saveToy(v:string) {
toy.value = v
}
</script>
<style scoped>
</style>
src/components/Son.vue
<template>
<h2>我是子组件Home</h2>
<div>
子组件的数据: <span>{{ toy }}</span>
</div>
<!-- 2. 定义点击按钮时的回调函数 -->
<button @click="sendToyToFather">把玩具发送给父组件</button>
</template>
<script lang="ts" setup name="Home">
import { ref } from 'vue';
const toy = ref("奥特曼")
// 1. 声明需要触发的父组件中提前定义好的自定义事件
const emit = defineEmits(['sendToy'])
function sendToyToFather() {
// 3. 触发父组件中定义的自定义事件, 并发携带参数
emit('sendToy', toy)
}
</script>
3. 通过获取父组件实例操作父组件的数据
src/components/Father.vue
<template>
<div class="app">
<h2>我是父组件</h2>
<h4>房产: {{ houseNum }}</h4>
<Son1 />
<Son2 />
</div>
</template>
<script lang="ts" setup name="Father">
import Son1 from '@/components/Son1.vue';
import Son2 from '@/components/Son2.vue';
import { ref } from 'vue';
let houseNum = ref(4)
// 1. 向外暴露 houseNum
defineExpose({
houseNum
})
</script>
<style scoped>
.app{
width: 100%;
height: 800px;
border-radius: 20px;
background-color: burlywood;
overflow: hidden;
}
</style>
src/components/Son1.vue
<template>
<div class="son">
<h2>我是儿子1组件</h2>
<!-- 1. 将 父组件实例 传递给回调函数 -->
<button @click="changHouseNum($parent)">父亲房产-1</button>
<h4>玩具: {{ computer }}</h4>
<h4>书: {{ book }}</h4>
</div>
</template>
<script lang="ts" setup name="Son">
import { ref } from 'vue';
const computer = ref("苹果笔记本")
let book = ref(6)
function changHouseNum(parent:any){
// 2. 操作父组件中的数据
parent.houseNum -= 1
}
</script>
<style scoped>
.son{
width: 1200px;
height: 250px;
border-radius: 20px;
background-color: cadetblue;
margin: 0 auto;
margin-top: 50px;
}
</style>
src/components/Son2.vue
<template>
<div class="grandson">
<h2>我是儿子2组件</h2>
<!-- 1. 将 父组件实例 传递给回调函数 -->
<button @click="changHouseNum($parent)">父亲房产-1</button>
<h4>玩具: {{ toy }}</h4>
<h4>书: {{ book }}</h4>
</div>
</template>
<script lang="ts" setup name="GrandSon">
import { ref } from 'vue';
const toy = ref("奥特曼")
let book = ref(3)
function changHouseNum(parent:any){
// 2. 操作父组件中的数据
parent.houseNum -= 1
}
</script>
<style scoped>
.grandson{
width: 1200px;
height: 250px;
border-radius: 20px;
background-color: brown;
margin: 0 auto;
margin-top: 50px;
}
</style>
3. v-model 父子组件通信原理
UI 组件库的底层大量使用呢 v-model 进行通信
v-model用在html标签上的作用是双向数据绑定v-model用在 组件上 的作用是用来通信
1. 双向数据绑定
<template>
<!-- v-model 的简单写法 -->
<input type="text" v-model="username">
<!-- v-model 的底层写法, :value 保证 username 在模版中的渲染, @input 保证在修改的时候同步修改 username-->
<input type="text" :value="username" @input="username = (<HTMLInputElement>$event.target).value">
</template>
<script lang="ts" setup name="ChildOne">
import { ref } from 'vue';
let username = ref("小明")
</script>
2. UI 组件库中的 input 组件可以写 v-model 来实现双向数据绑定的原理
1. 在页面中使用 UI 组件库
<template>
<!-- 1. 只在组件上写 v-model 是无法实现双向数据绑定的, 需要在子组件中手动实现一些逻辑 -->
<MyInput />
<br>
<!-- 2. 解决办法(底层原理): :modelVlue 来实现页面中的渲染, @update:modelValue(update:modelValue是自定义事件名) 来实现修改 input 中的内容的同时修改 username -->
<!-- 在 Vue2 中是 :value @input Vue3 改成了 :modelValue @update:modelValue-->
<MyInput :modelValue="username" @update:modelValue="username = $event" />
<!-- 3. 上面解决办法的简单写法 -->
<MyInput v-model="username"/>
</template>
<script lang="ts" setup name="ChildOne">
import { ref } from 'vue';
import MyInput from '@/components/MyInput.vue';
let username = ref("小明")
</script>
2. UI 组件库中已经实现的底层逻辑, 以让使用 UI 组件库的人, 可以很简单的使用 v-model 指令就可以完成双向数据绑定
<template>
<!-- 3. 绑定 数据 和 自定义事件 -->
<input type="text" :value="modelValue" @input="emit('update:modelValue', (<HTMLInputElement>$event.target).value)">
</template>
<script lang="ts" setup name="MyInput">
// 1. 接收父组件传递的数据
defineProps(['modelValue'])
// 2. 接收父组件传递的自定义事件
const emit = defineEmits(["update:modelValue"])
</script>
3. 修改默认名字 (modelValue)
1. 在页面中使用 UI 组件库
<template>
<!-- 1. v-model:username 是将之前的 modelValue 改为 username, 这样可以传递多个双向数据绑定的数据 -->
<MyInput v-model:username="username" v-model:password="password"/>
</template>
<script lang="ts" setup name="ChildOne">
import { ref } from 'vue';
import MyInput from '@/components/MyInput.vue';
let username = ref("小明")
let password = ref("")
</script>
2. UI 组件库的源码实现逻辑
<template>
<!-- 3. 绑定 自定义数据 和 自定义事件 -->
用户名: <input type="text" :value="username" @input="emit('update:username', (<HTMLInputElement>$event.target).value)">
<br>
密码: <input type="password" :value="password" @input="emit('update:password', (<HTMLInputElement>$event.target).value)">
</template>
<script lang="ts" setup name="MyInput">
// 1. 接收父组件自定义的数据
defineProps(['username','password'])
// 2. 接收父组件传递的自定义事件
const emit = defineEmits(["update:username","update:password"])
</script>
4. $event 的总结
- 对于原生事件中的$event:
$event就是事件对象, 可以.target - 对于自定义事件的$event:
$event就是触发事件时所传递的数据, 不能.target
4. solt 插槽 父子组件通信
1. 默认插槽
src/components/Father.vue
<template>
<div class="app">
<h3>父组件</h3>
<div class="items">
<Son title="游戏排行榜">
<!-- 1. 将这些标签, 替换子组件的 solt 插槽标签 -->
<ul>
<li v-for="g in games" :key="g.id">{{ g.name }}</li>
</ul>
</Son>
<Son title="美食排行榜"></Son>
<Son title="电影排行榜"></Son>
</div>
</div>
</template>
<script lang="ts" setup name="Father">
import Son from './Son.vue';
let games = [
{
id: '001',
name:"王者荣耀",
imageUrl: ""
},
{
id: '002',
name:"英雄联盟",
imageUrl: ""
},
{
id: '003',
name:"和平精英",
imageUrl: ""
},
]
</script>
<style scoped>
.app{
width: 100%;
height: 800px;
background-color: gray;
}
.items{
display: flex;
justify-content: space-around;
}
</style>
src/components/Son.vue
<template>
<div class="category">
<h2>{{ title }}</h2>
<!-- 1. 使用默认插槽 slot 标签占位 -->
<slot>如果父组件没有传递标签内容, 则显示这些文字内容</slot>
</div>
</template>
<script lang="ts" setup name="Son">
defineProps(["title"])
</script>
<style scoped>
.category{
background-color: skyblue;
border-radius: 10px;
box-shadow: 0 0 10px;
padding: 10px;
width: 200px;
height: 300px;
}
h2 {
width: 100%;
height: 40px;
background-color: orange;
}
</style>
2. 具名插槽
src/components/Father.vue
<template>
<div class="app">
<h3>父组件</h3>
<div class="items">
<Son title="游戏排行榜">
<!-- 1. v-slot 只能放在 组件标签 / template 标签中 -->
<!-- 2. v-slot:content 会替换组件中 name="content" 的 slot -->
<template v-slot:content>
<ul>
<li v-for="g in games" :key="g.id">{{ g.name }}</li>
</ul>
</template>
<!-- 2. #title 是 v-slot:title 的简写形式, 它会替换组件中 name="title" 的 slot -->
<template #title>
<h2>游戏排行榜</h2>
</template>
</Son>
</div>
</div>
</template>
<script lang="ts" setup name="Father">
import Son from './Son.vue';
import {reactive} from "vue";
let games = reactive([
{
id: '001',
name: "王者荣耀",
imageUrl: ""
},
{
id: '002',
name: "英雄联盟",
imageUrl: ""
},
{
id: '003',
name: "和平精英",
imageUrl: ""
},
])
</script>
<style scoped>
.app {
width: 100%;
height: 800px;
background-color: gray;
}
.items {
display: flex;
justify-content: space-around;
}
h2 {
width: 100%;
height: 40px;
background-color: orange;
}
</style>
src/components/Son.vue
<template>
<div class="category">
<!-- 1. 使用具名插槽 slot 标签占位 -->
<slot name="title">默认标题</slot>
<slot name="content">默认内容</slot>
</div>
</template>
<script lang="ts" setup name="Son">
</script>
<style scoped>
.category{
background-color: skyblue;
border-radius: 10px;
box-shadow: 0 0 10px;
padding: 10px;
width: 200px;
height: 300px;
}
</style>
3. 作用域插槽
数据在子组件中, 但是父组件需要拿到子组件中的数据来生成标签结构
1. 默认作用域插槽
src/components/Index.vue
<template>
<div class="app">
<h3>父组件</h3>
<div class="items">
<!-- 无序列表形式的 Game 组件 -->
<Game>
<!-- 1. 使用 s-slot="" 来接收插槽传递过来的数据 -->
<template v-slot="params">
<ul>
<li v-for="g in params.games" :key="g.id">{{ g.name }}</li>
</ul>
</template>
</Game>
<!-- 有序列表形式的 Game 组件 -->
<Game>
<!-- 1. 使用 s-slot="" 来接收插槽传递过来的数据, 可以使用 {games} 来解构 -->
<template v-slot="{games}">
<ol>
<li v-for="g in games" :key="g.id">{{ g.name }}</li>
</ol>
</template>
</Game>
</div>
</div>
</template>
<script lang="ts" setup name="Index">
import Game from "@/components/Game.vue";
</script>
<style scoped>
.app {
width: 100%;
height: 800px;
background-color: gray;
}
.items {
display: flex;
justify-content: space-around;
}
</style>
src/components/Game.vue
<template>
<div class="game">
<h2>游戏排行榜</h2>
<!-- 1. :games="games", 将子组件的数据传递给插槽的使用者 -->
<slot :games="games">默认插槽内容</slot>
</div>
</template>
<script lang="ts" setup name="Game">
import {reactive} from "vue";
let games = reactive([
{
id: '001',
name: "王者荣耀",
imageUrl: ""
},
{
id: '002',
name: "英雄联盟",
imageUrl: ""
},
{
id: '003',
name: "和平精英",
imageUrl: ""
},
])
</script>
<style scoped>
.game{
background-color: skyblue;
border-radius: 10px;
box-shadow: 0 0 10px;
padding: 10px;
width: 200px;
height: 300px;
}
h2 {
width: 100%;
height: 40px;
background-color: orange;
}
</style>
2. 具名作用域插槽
src/components/Index.vue
<template>
<div class="app">
<h3>父组件</h3>
<div class="items">
<!-- 无序列表形式的 Game 组件 -->
<Game>
<!-- 1. 使用 s-slot:具名插槽的名字="" 来接收插槽传递过来的数据 -->
<template v-slot:game-slot="params">
<ul>
<li v-for="g in params.games" :key="g.id">{{ g.name }}</li>
</ul>
</template>
</Game>
<!-- 有序列表形式的 Game 组件 -->
<Game>
<!-- 1. 使用 s-slot:具名插槽的名字="", 可以使用 {games} 来解构 -->
<template v-slot:game-slot="{games}">
<ol>
<li v-for="g in games" :key="g.id">{{ g.name }}</li>
</ol>
</template>
</Game>
</div>
</div>
</template>
<script lang="ts" setup name="Index">
import Game from "@/components/Game.vue";
</script>
<style scoped>
.app {
width: 100%;
height: 800px;
background-color: gray;
}
.items {
display: flex;
justify-content: space-around;
}
</style>
src/components/Game.vue
<template>
<div class="game">
<h2>游戏排行榜</h2>
<!-- 1. :games="games", 将子组件的数据传递给插槽的使用者 -->
<slot name="game-slot" :games="games">默认插槽内容</slot>
</div>
</template>
<script lang="ts" setup name="Game">
import {reactive} from "vue";
let games = reactive([
{
id: '001',
name: "王者荣耀",
imageUrl: ""
},
{
id: '002',
name: "英雄联盟",
imageUrl: ""
},
{
id: '003',
name: "和平精英",
imageUrl: ""
},
])
</script>
<style scoped>
.game{
background-color: skyblue;
border-radius: 10px;
box-shadow: 0 0 10px;
padding: 10px;
width: 200px;
height: 300px;
}
h2 {
width: 100%;
height: 40px;
background-color: orange;
}
</style>
3. 简写形式
src/components/Index.vue
<template>
<div class="app">
<h3>父组件</h3>
<div class="items">
<!-- 无序列表形式的 Game 组件 -->
<Game>
<!-- 1. 使用 #具名插槽的名字="" 来接收插槽传递过来的数据 -->
<template #game-slot="params">
<ul>
<li v-for="g in params.games" :key="g.id">{{ g.name }}</li>
</ul>
</template>
</Game>
<!-- 有序列表形式的 Game 组件 -->
<Game>
<!-- 1. 使用 #具名插槽的名字="", 可以使用 {games} 来解构 -->
<template #game-slot="{games}">
<ol>
<li v-for="g in games" :key="g.id">{{ g.name }}</li>
</ol>
</template>
</Game>
</div>
</div>
</template>
<script lang="ts" setup name="Index">
import Game from "@/components/Game.vue";
</script>
<style scoped>
.app {
width: 100%;
height: 800px;
background-color: gray;
}
.items {
display: flex;
justify-content: space-around;
}
</style>
src/components/Game.vue
<template>
<div class="game">
<h2>游戏排行榜</h2>
<!-- 1. :games="games", 将子组件的数据传递给插槽的使用者 -->
<slot name="game-slot" :games="games">默认插槽内容</slot>
</div>
</template>
<script lang="ts" setup name="Game">
import {reactive} from "vue";
let games = reactive([
{
id: '001',
name: "王者荣耀",
imageUrl: ""
},
{
id: '002',
name: "英雄联盟",
imageUrl: ""
},
{
id: '003',
name: "和平精英",
imageUrl: ""
},
])
</script>
<style scoped>
.game{
background-color: skyblue;
border-radius: 10px;
box-shadow: 0 0 10px;
padding: 10px;
width: 200px;
height: 300px;
}
h2 {
width: 100%;
height: 40px;
background-color: orange;
}
</style>
5. 任意组件通信
1. mitt ( 发布订阅模式 )
1. 下载模块
npm i mitt
**2. 实例化 mitt **
src/utils/emitter.ts
import mitt from "mitt";
const emitter = mitt()
export default emitter
3. 常用方法
// 绑定事件
emitter.on('add',(value)=>{
})
// 触发事件
emitter.emit('add')
// 解绑单个事件
setTimeout(()=>{
emitter.off('add')
},3000)
// 清空所有事件
setTimeout(()=>{
emitter.all.clear()
},4000)
5. 示例
需求: 弟弟接收哥哥发送过来的数据
- 谁接收数据, 谁绑定事件
- 谁发送数据, 谁触发事件
src/components/Father.vue
<template>
<div class="app">
<ChildOne />
<ChildTwo />
</div>
</template>
<script lang="ts" setup name="App">
import ChildOne from '@/components/ChildOne.vue';
import ChildTwo from '@/components/ChildTwo.vue';
</script>
<style scoped>
</style>
src/components/Child1.vue
<template>
<h2>我是子组件1</h2>
<div>
子组件的数据: <span>{{ computer }}</span>
</div>
<button>把电脑给父组件</button>
<!-- 2. 定义按钮的点击事件时触发的回调函数 -->
<button @click="sendComputer">把电脑给兄弟组件</button>
</template>
<script lang="ts" setup name="ChildOne">
import { ref } from 'vue';
// 1. 导入 emitter
import emitter from '@/utils/emitter';
const computer = ref("苹果笔记本")
// 3. 定义回调函数
function sendComputer(){
// 4. 使用 mitt 触发 get-computer 事件, 并传递参数
emitter.emit('get-computer', computer)
}
</script>
src/components/Child2.vue
<template>
<h2>我是子组件2</h2>
<div>
子组件的数据: <span>{{ toy }}</span>
</div>
<div>
<!-- 3. 使用从兄弟组件传递过来的数据 -->
接收到来自兄弟组件的数据: <span>{{ computer }}</span>
</div>
<button>把玩具给父组件</button>
</template>
<script lang="ts" setup name="ChildOne">
import { ref,onUnmounted } from 'vue';
// 1. 导入 emitter
import emitter from '@/utils/emitter';
const toy = ref("奥特曼")
let computer = ref(' ')
// 2. 绑定用来接收数据的 get-computer 事件及回调函数
emitter.on('get-computer',(v:string)=>{
computer.value = v
})
// 3. 组件卸载时, 需要解绑事件, 释放内存
onUnmounted(()=>{
emitter.off('get-computer')
})
</script>
2. pinia
集中式状态 (数据) 管理工具 pinia (类似 vuex)
6. 祖孙组件通信
1. 祖传儿再传孙
1. props
使用 props 一层一层向下传递数据
src/components/Father.vue
<template>
<div class="app">
<h2>我是父组件</h2>
<h4>{{ a }} -- {{ b }} -- {{ c }} -- {{ d }}</h4>
<!-- 1. 将数据传给儿子组件, 传了四个参数 -->
<Son :a="a" :b="b" :c="c" :d="d"/>
</div>
</template>
<script lang="ts" setup name="App">
import Son from '@/components/Son.vue';
import { ref } from 'vue';
const a = ref(0)
const b = ref(1)
const c = ref(2)
const d = ref(3)
</script>
<style scoped>
.app{
width: 100%;
height: 800px;
border-radius: 20px;
background-color: burlywood;
overflow: hidden;
}
</style>
src/components/Son.vue
<template>
<div class="son">
<h2>我是儿子组件</h2>
<h4>{{ a }} -- {{ b }}</h4>
<h4>其他: {{ $attrs }}</h4>
<!-- 2. 将数据传给孙子组件 -->
<GrandSon :a="a" :b="b"/>
</div>
</template>
<script lang="ts" setup name="Son">
import GrandSon from '@/components/GrandSon.vue';
// 1. 接收从父组件传递过来的数据, 只将2个放在props, 其他的被保存在 $attrs 中
defineProps(["a", "b"])
</script>
<style scoped>
.son{
width: 1200px;
height: 600px;
border-radius: 20px;
background-color: cadetblue;
margin: 0 auto;
margin-top: 50px;
}
</style>
src/components/GrandSon.vue
<template>
<div class="grandson">
<h2>我是孙子组件</h2>
<!-- 2. 展示数据 -->
<h4>{{ a }} -- {{ b }}</h4>
</div>
</template>
<script lang="ts" setup name="GrandSon">
// 1. 接收从父亲组件传递过来的数据
defineProps(["a", "b"])
</script>
<style scoped>
.grandson{
width: 1100px;
height: 400px;
border-radius: 20px;
background-color: brown;
margin: 0 auto;
margin-top: 50px;
}
</style>
2. $attrs
src/components/Father.vue
<template>
<div class="app">
<h2>我是父组件</h2>
<h4>{{ a }} -- {{ b }} -- {{ c }} -- {{ d }}</h4>
<!-- 1. 将数据传给儿子组件, 传了四个参数 -->
<Son :a="a" :b="b" :c="c" :d="d"/>
</div>
</template>
<script lang="ts" setup name="App">
import Son from '@/components/Son.vue';
import { ref } from 'vue';
const a = ref(0)
const b = ref(1)
const c = ref(2)
const d = ref(3)
</script>
<style scoped>
.app{
width: 100%;
height: 800px;
border-radius: 20px;
background-color: burlywood;
overflow: hidden;
}
</style>
src/components/Son.vue
<template>
<div class="son">
<h2>我是儿子组件</h2>
<h4>{{ a }} -- {{ b }}</h4>
<h4>其他: {{ $attrs }}</h4>
<!-- 2. 儿子不关心父亲组件传递的数据, 直接全部以 props 的形式传给孙子组件 -->
<GrandSon v-bind="$attrs"/>
</div>
</template>
<script lang="ts" setup name="Son">
import GrandSon from '@/components/GrandSon.vue';
</script>
<style scoped>
.son{
width: 1200px;
height: 600px;
border-radius: 20px;
background-color: cadetblue;
margin: 0 auto;
margin-top: 50px;
}
</style>
src/components/GrandSon.vue
<template>
<div class="grandson">
<h2>我是孙子组件</h2>
<!-- 2. 展示 props 中的数据 -->
<h4>{{ a }} -- {{ b }}</h4>
<!-- 3. 其他数据可以从 $attrs 中看到 -->
<h4>其他: {{ $attrs }}</h4>
</div>
</template>
<script lang="ts" setup name="GrandSon">
// 1. 接收从父亲组件传递过来的数据, 存放到 props
defineProps(["a", "b"])
</script>
<style scoped>
.grandson{
width: 1100px;
height: 400px;
border-radius: 20px;
background-color: brown;
margin: 0 auto;
margin-top: 50px;
}
</style>
2. 孙传父再传祖
1. 自定义函数
src/components/Father.vue
<template>
<div class="app">
<h2>我是父组件</h2>
<h4>接收来自孙子组件的数据: {{ defaultData }}</h4>
<!-- 1. 自定义函数传递给儿子, 再由儿子传递给孙子 -->
<Son :sendData="saveData"/>
</div>
</template>
<script lang="ts" setup name="App">
import Son from '@/components/Son.vue';
import { ref } from 'vue';
let defaultData = ref(0)
// 2. 定义当孙子触发自定义函数时要执行的回调函数
function saveData(value:number){
defaultData.value = value
}
</script>
<style scoped>
.app{
width: 100%;
height: 800px;
border-radius: 20px;
background-color: burlywood;
overflow: hidden;
}
</style>
src/components/Son.vue
<template>
<div class="son">
<h2>我是儿子组件</h2>
<!-- 2. 将自定义函数传给孙子组件 -->
<GrandSon v-bind="$attrs"/>
</div>
</template>
<script lang="ts" setup name="Son">
import GrandSon from '@/components/GrandSon.vue';
</script>
<style scoped>
.son{
width: 1200px;
height: 600px;
border-radius: 20px;
background-color: cadetblue;
margin: 0 auto;
margin-top: 50px;
}
</style>
src/components/GrandSon.vue
<template>
<div class="grandson">
<h2>我是孙子组件</h2>
<!-- 2. 点击按钮时, 触发 sendData 这个自定义函数, 并将数据传过去 -->
<button @click="sendData(grandSonData)">点击把数据传给祖宗组件</button>
</div>
</template>
<script lang="ts" setup name="GrandSon">
import { ref } from 'vue';
let grandSonData = ref(100)
// 1. 接收来自父组件传递的 sendData 自定义函数
defineProps(["sendData"])
</script>
<style scoped>
.grandson{
width: 1100px;
height: 400px;
border-radius: 20px;
background-color: brown;
margin: 0 auto;
margin-top: 50px;
}
</style>
3. 祖直接传孙
src/components/GrandFather.vue
<template>
<div class="app">
<h2>我是祖宗组件</h2>
<h4>金钱: {{ money }} 万</h4>
<h4>车: 一辆{{ car.brand }}车, 价值{{ car.price }}万</h4>
<Son />
</div>
</template>
<script lang="ts" setup name="App">
import Son from '@/components/Son.vue';
import { ref,provide, reactive } from 'vue';
let money = ref(100)
let car = reactive({
brand: "奔驰",
price: 40
})
// 1. 向所有后代提供 数据
provide('money', money)
provide('car', car)
</script>
<style scoped>
.app{
width: 100%;
height: 800px;
border-radius: 20px;
background-color: burlywood;
overflow: hidden;
}
</style>
src/components/Father.vue
<template>
<div class="son">
<h2>我是父亲组件</h2>
<GrandSon />
</div>
</template>
<script lang="ts" setup name="Father">
import GrandSon from '@/components/GrandSon.vue';
</script>
<style scoped>
.son{
width: 1200px;
height: 500px;
border-radius: 20px;
background-color: cadetblue;
margin: 0 auto;
margin-top: 50px;
}
</style>
src/components/GrandSon.vue
<template>
<div class="grandson">
<h2>我是孙子组件</h2>
<h4>金钱: {{ money }} 万</h4>
<h4>车: 一辆{{ car.brand }}车, 价值{{ car.price }}万</h4>
</div>
</template>
<script lang="ts" setup name="GrandSon">
import { inject } from 'vue';
// 1. 注入 祖宗组件 提供给后代的 数据 和 函数, 并设置默认值
const money = inject('money')
const car = inject('car',{brand:'未知', price:0})
</script>
<style scoped>
.grandson{
width: 1100px;
height: 250px;
border-radius: 20px;
background-color: brown;
margin: 0 auto;
margin-top: 50px;
}
</style>
4. 孙直接传祖
1. 示例一
祖宗组件中定义一个用于接受数据的自定义函数, 然后提供给后代组件, 孙子组件中触发这个函数, 将数据携带给祖宗组件
src/components/GrandFather.vue
<template>
<div class="app">
<h2>我是祖宗组件</h2>
<h4>玩具: {{ toy }}</h4>
<Son />
</div>
</template>
<script lang="ts" setup name="GrandFather">
import Son from '@/components/Son.vue';
import { ref,provide } from 'vue';
let toy = ref('')
// 1. 定义自定义函数, 用来接收孙子组件
function getToy(value:string){
console.log("获取来自孙子组件的数据: ",value);
toy.value = value
}
// 2. 向所有后代提供 数据 和 自定义函数
provide('getToy',getToy)
</script>
<style scoped>
.app{
width: 100%;
height: 800px;
border-radius: 20px;
background-color: burlywood;
overflow: hidden;
}
</style>
src/components/GrandSon.vue
<template>
<div class="grandson">
<h2>我是孙子组件</h2>
<button @click="getToy(toy)">点击发送玩具给祖宗组件</button>
<h4>玩具: {{ toy }}</h4>
</div>
</template>
<script lang="ts" setup name="GrandSon">
import { inject, ref } from 'vue';
const toy = ref('奥特曼')
// 1. 注入 祖宗组件 提供给后代的 数据 和 函数, 并设置默认值
const getToy = inject('getToy') as (value:string)=>{}
</script>
<style scoped>
.grandson{
width: 1100px;
height: 250px;
border-radius: 20px;
background-color: brown;
margin: 0 auto;
margin-top: 50px;
}
</style>
2. 示例二
更紧凑的写法
src/components/GrandFather.vue
<template>
<div class="app">
<h2>我是祖宗组件</h2>
<h4>金钱: {{ money }} 万</h4>
<h4>车: 一辆{{ car.brand }}车, 价值{{ car.price }}万</h4>
<Son />
</div>
</template>
<script lang="ts" setup name="App">
import Son from '@/components/Son.vue';
import { ref,provide, reactive } from 'vue';
let money = ref(100)
let car = reactive({
brand: "奔驰",
price: 40
})
function updateMoney(value:number) {
money.value -= value
}
// 1. 向所有后代提供 数据 和 函数, 函数用来操作祖宗组件内的数据
provide('moneyContext', {money,updateMoney})
provide('car', car)
</script>
<style scoped>
.app{
width: 100%;
height: 800px;
border-radius: 20px;
background-color: burlywood;
overflow: hidden;
}
</style>
src/components/GrandSon.vue
<template>
<div class="grandson">
<h2>我是孙子组件</h2>
<button @click="updateMoney(10)">修改祖宗的钱</button>
<h4>金钱: {{ money }} 万</h4>
<h4>车: 一辆{{ car.brand }}车, 价值{{ car.price }}万</h4>
</div>
</template>
<script lang="ts" setup name="GrandSon">
import { inject } from 'vue';
// 1. 注入 祖宗组件 提供给后代的 数据 和 函数, 并设置默认值
const {money,updateMoney} = inject('moneyContext', {money:0, updateMoney:(value:number)=>{}})
const car = inject('car',{brand:'未知', price:0})
</script>
<style scoped>
.grandson{
width: 1100px;
height: 250px;
border-radius: 20px;
background-color: brown;
margin: 0 auto;
margin-top: 50px;
}
</style>

浙公网安备 33010602011771号