vue3组件之间的通信方式
在vue3中,组件间的通信主要场景可以分为父子组件通信、兄弟(跨)组件通信、隔代组件通信
父子组件通信:props/$emit、Event Bus、v-model、useAttrs()、ref/$parent、Pinia
兄弟(跨)组件通信:Event Bus、Pinia
隔代组件通信:Event Bus、provide/inject、Pinia
1、props:父子组件通信——父传子
父组件通过props属性向子组件传递数据
父组件:
<template>
<h1>父组件{{ name }}</h1>
<hr>
<!-- 使用子组件,给子组件传值 -->
<Child :name="name"/>
</template>
<script setup lang="ts">
import { ref } from 'vue';
//引入子组件
import Child from './Child.vue';
const name = ref('zzh')
</script>
<style scoped>
</style>
子组件Child:
<template>
<!--{{props.name}}---{{name}}可以省略props--->
<h2>子组件{{ name }}</h2>
</template>
<script setup lang="ts">
//需要使用到defineProps方法去接受父组件传递过来的数据
//defineProps是Vue3提供方法,不需要引入直接使用
//props:可以实现父子组件通信,props数据是只读的!!!
const props = defineProps(['name']) //数组|对象写法都可以
</script>
<style scoped>
</style>
2、$emit:父子组件通信——子传父
子组件使用$emit触发一个事件,将数据传递回父组件。父组件通过v-on或@监听并响应这个事件。
父组件:
<template>
<h1>父组件{{ money }}</h1>
<hr>
<!-- 绑定自定义事件changeData:实现子组件给父组件传递数据 -->
<Event @changeData="handle" :money="money"/>
</template>
<script setup lang="ts">
import { ref } from 'vue';
//引入子组件
import Event from './Event.vue';
let money = ref(1000)
//事件回调
const handle = (val)=>{
money.value = val
}
</script>
<style scoped>
</style>
子组件Event:
<template>
<h2>子组件{{ money }}</h2>
<button @click="handleChange">+100</button>
</template>
<script setup lang="ts">
let props = defineProps(['money'])
//利用defineEmits方法返回函数触发自定义事件
let $emit = defineEmits(['changeData'])
//按钮点击回调
const handleChange = ()=>{
//第一个参数:事件类型 第二个|三个|N参数即为注入数据
$emit('changeData',props.money+100)
}
</script>
<style scoped>
</style>
3、Event Bus-mitt:父子、兄弟、隔代组件通信
vue3使用eventBus功能常常借用第三方库mitt,mitt 是一个简单而强大的事件总线库,用于在组件之间进行事件的发布和订阅
mitt主要方法:
emit(name,data) // 触发事件,两个参数:name:触发的方法名,data:需要传递的参数
on(name,callback) // 绑定事件,两个参数:name:绑定的方法名,callback:触发后执行的回调函数
off(name) // 解绑事件,一个参数:name:需要解绑的方法名
1、安装mitt:确保你的开发环境中已经安装了Node.js和npm
npm install --save mitt
2、在src文件夹下新建bus文件夹,index.ts文件
//引入mitt插件:mitt是一个方法,方法执行会返回bus对象
import mitt from 'mitt';
const $bus = mitt();
export default $bus;
3、在文件中直接引入然后使用
父组件:
<template>
<h1>父组件BUS----</h1>
<hr>
<Child1 />
<hr>
<Child2 />
</template>
<script setup lang="ts">
import Child1 from './Child1.vue';
import Child2 from './Child2.vue';
</script>
<style scoped>
</style>
子组件1Child1:
<template>
<p>子组件1</p>
<button @click = "handle">to 1 an apple</button>
</template>
<script setup lang="ts">
//引入$bus对象
import $bus from '@/bus';
//点击按钮回调
const handle = ()=>{
$bus.emit('apple','1000apple')
}
</script>
<style scoped>
</style>
子组件2Child2:
<template>
<p>子组件2{{ apple }}</p>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import $bus from '@/bus';
let apple = ref(null)
//组件挂载完毕的时候,当前组件绑定一个事件,接受将来兄弟组件传递的数据
onMounted(()=>{
//第一个参数:即为事件类型 第二个参数:即为事件回调
$bus.on('apple',(val)=>{
console.log(val);
apple.value = val
})
})
</script>
<style scoped>
</style>
4、v-model:父子组件数据同步
父组件:
<template>
<h1>父组件{{ money }}</h1>
{{ age }}
<hr>
<!-- props:父亲给儿子数据 -->
<!-- <Child :modelValue="money" @update:modelValue="handler"></Child> -->
<!--
v-model组件身上使用
第一:相当有给子组件传递props[modelValue] = 10000
第二:相当于给子组件绑定自定义事件update:modelValue
-->
<Child v-model="money"/>
<hr>
<Child1 v-model="age"/>
</template>
<script setup lang="ts">
//v-model指令:收集表单数据,数据双向绑定
//v-model也可以实现组件之间的通信,实现父子组件数据同步的业务
//父亲给子组件数据 props
//子组件给父组件数据 自定义事件
import Child from './Child.vue';
import Child1 from './Child1.vue';
import { ref } from 'vue';
let money = ref(10000)
let age = ref(10)
const handler=(val)=>{
money.value = val
age.value = val
}
</script>
<style scoped>
</style>
子组件1Child :
<template>
<h2>子组件1---{{ modelValue }}</h2>
<button @click="handle">money+100</button>
</template>
<script setup lang="ts">
//接受props
let props = defineProps(['modelValue'])
let emit = defineEmits(['update:modelValue'])
//子组件内部按钮的点击回调
const handle =()=>{
//触发自定义事件
emit('update:modelValue',props.modelValue+100)
}
</script>
<style scoped>
</style>
子组件2Child1:
<template>
<h2>子组件2</h2>
{{ modelValue
}}
<button @click="handle">age+1</button>
</template>
<script setup lang="ts">
let props= defineProps(['modelValue'])
let emit = defineEmits(['update:modelValue'])
const handle=()=>{
emit('update:modelValue',props.modelValue+1)
}
</script>
<style scoped>
</style>
5、useAttrs():父子组件通信
主要用于解决组件之间的属性继承和事件监听问题,特别是在封装可复用组件时非常有用。
$attrs :包含没有被作为prop定义的特性(attributes)。当一个组件没有声明接受某些属性时,传递给该组件的额外属性会被收集在 $attrs对象中。比如:如果有一个封装好的组件,想让它自动传递所有未被处理的属性给其内部的子组件,可以在组件的根元素上使用v-bind=" $attrs"。
在子组件中,我们通过useAttrs()函数来接收父组件传递的属性和事件。在setup()函数中,我们可以直接使用这些属性和事件,无需再声明为props和emits,然后将它们返回。
父组件:
<template>
<h1>父组件</h1>
<el-button type="primary" :icon="Delete" title="删除按钮"/>
<!-- 自定义组件,给子组件传参,传事件 -->
<HintButton type="primary" :icon="Delete" title="删除按钮" @xxx="handler" />
</template>
<script setup lang="ts">
import { Delete} from '@element-plus/icons-vue'
import HintButton from './HintButton.vue';
//按钮点击的回调
const handler = ()=>{
alert(12306);
}
</script>
<style scoped>
</style>
子组件HintButton:
<template>
<div :title="title">
<!-- 绑定传过来的参数和事件 -->
<el-button :="$attrs"></el-button>
</div>
</template>
<script setup lang="ts">
//引入useAttrs方法:获取组件标签身上属性与事件
import {useAttrs} from 'vue';
//此方法执行会返回一个对象
let $attrs = useAttrs();
//万一用props接受title
let props =defineProps(['title']);
//props与useAttrs方法都可以获取父组件传递过来的属性与属性值
//但是props接受了useAttrs方法就获取不到了
console.log($attrs);
</script>
<style scoped>
</style>
6、ref、$parent:父子之间通信
ref:
父组件通过$ref可以直接引用子组件的方法或属性。ref如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;
如果用在子组件上,引用就指向组件实例。
注意:但这通常用于操作子组件而不是传递数据,因为这可能导致代码紧密耦合
$parent:
可以在子组件内部获取到父组件的实例
父组件:
<template>
<p>我是父亲{{ money }}</p>
<button @click="handle">儿子给我100</button>
<hr>
<!-- 在父组件拿子组件的实例 -->
<Son ref="demo"/>
<hr>
<Daughter />
</template>
<script setup lang="ts">
//ref:可以获取真实的DOM节点,可以获取到子组件实例VC
//$parent:可以在子组件内部获取到父组件的实例
//引入子组件
import { ref } from 'vue';
import Son from './Son.vue'
import Daughter from './Daughter.vue';
//父组件钱数
let money = ref(100000000)
//获取子组件的实例
let demo = ref()
//父组件内部按钮点击回调
const handle = ()=>{
money.value +=100
demo.value.money -= 100
}
//对外暴露-让女儿子组件在内部获取到父亲的钱数
defineExpose({
money
})
</script>
<style scoped>
</style>
儿子组件Son:
<template>
<p>我是儿子{{ money }}</p>
</template>
<script setup lang="ts">
import { ref } from 'vue';
//儿子钱数
let money = ref(888888)
//组件内部数据对外关闭的,别人不能访问
//如果想让外部访问需要通过defineExpose方法对外暴露
//对外暴露-让父亲组件在内部获取到儿子的钱数
defineExpose({
money
})
</script>
<style scoped>
</style>
女儿组件Daughter:
<template>
<p>我是女儿{{ money }}</p>
<!-- 在子组件点击时注入$parent,获取当前组件的父组件的实例 -->
<button @click = "handle($parent)">找爸爸要200</button>
</template>
<script setup lang="ts">
import { ref } from 'vue';
//闺女钱数
let money = ref(66666666666)
//闺女按钮点击回调
const handle = ($parent)=>{
money.value +=200
$parent.money -=200
}
</script>
<style scoped>
</style>
7、provide、inject:隔代组件通信
父组件可以使用provide选项来提供数据,而无需通过中间的子组件层级,子组件则通过inject选项注入这些数据。
父组件:
<template>
<div class="box">
<h1>Provide与Inject{{car}}</h1>
<hr />
<Child></Child>
</div>
</template>
<script setup lang="ts">
import Child from "./Child.vue";
//vue3提供provide(提供)与inject(注入),可以实现隔辈组件传递数据
import { ref, provide } from "vue";
let car = ref("法拉利");
//祖先组件给后代组件提供数据
//两个参数:第一个参数就是提供的数据key
//第二个参数:祖先组件提供数据
provide("TOKEN", car);
</script>
<style scoped>
.box {
width: 100vw;
height: 600px;
background: skyblue;
}
</style>
子组件Child:
<template>
<div class="child">
<h1>我是子组件1</h1>
<Child></Child>
</div>
</template>
<script setup lang="ts">
import Child from './GrandChild.vue';
</script>
<style scoped>
.child{
width: 300px;
height: 400px;
background: yellowgreen;
}
</style>
孙子组件GrandChild:
<template>
<div class="child1">
<h1>孙子组件</h1>
<p>{{car}}</p>
<button @click="updateCar">更新数据</button>
</div>
</template>
<script setup lang="ts">
import {inject} from 'vue';
//注入祖先组件提供数据
//需要参数:即为祖先提供数据的key
let car = inject('TOKEN');
const updateCar = ()=>{
car.value = '自行车';
}
</script>
<style scoped>
.child1 {
width: 200px;
height: 200px;
background: red;
}
</style>
8、Pinia:父子、隔代、兄弟组件通信
pinia:集中式管理状态容器,可以实现任意组件之间通信!!!
核心概念:state、actions、getters
pinia写法:选择式API、组合式API
选项式API:
组合式API:
1、安装pinia
npm install pinia
2、创建大仓库index.ts:
在src文件夹下新建store文件夹,新建index.ts文件(大仓库:管理小仓库)
store文件夹下新建modules文件夹
//创建大仓库
import { createPinia } from 'pinia';
//createPinia方法可以用于创建大仓库
let store = createPinia();
//对外暴露,安装仓库
export default store;
3、入口文件引入仓库main.ts
//引入仓库
import store from './store'
app.use(store)
4、选择式API——创建小仓库
modules文件夹下新建info.ts文件
//定义info小仓库
import { defineStore } from "pinia";
//第一个参数:小仓库名字 第二个参数:小仓库配置对象
//defineStore方法执行会返回一个函数,函数作用就是让组件可以获取到仓库数据
let useInfoStore = defineStore("info", {
//存储数据:state
state: () => {
return {
count: 99,
arr: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
}
},
//修改数据
actions: {
//注意:函数没有context上下文对象
//没有commit、没有mutations去修改数据
updateNum(a: number, b: number) {
this.count += a;
}
},
//计算属性
getters: {
total() {
let result:any = this.arr.reduce((prev: number, next: number) => {
return prev + next;
}, 0);
return result;
}
}
});
//对外暴露方法
export default useInfoStore;
组件中引入使用:
<template>
<div class="child">
<h1>{{ infoStore.count }}---{{infoStore.total}}</h1>
<button @click="updateCount">点击我修改仓库数据</button>
</div>
</template>
<script setup lang="ts">
import useInfoStore from "../../store/modules/info";
//获取小仓库对象
let infoStore = useInfoStore();
console.log(infoStore);
//修改数据方法
const updateCount = () => {
//infoStore.count++
//infoStore.$patch({
// count:1111
// })
//仓库调用自身的方法去修改仓库的数据
infoStore.updateNum(66,77);
};
</script>
<style scoped>
.child {
width: 200px;
height: 200px;
background: yellowgreen;
}
</style>
5、组合式API
modules文件夹下新建todo.ts文件
//定义组合式API仓库
import { defineStore } from "pinia";
import { ref, computed,watch} from 'vue';
//创建小仓库
//第一个仓库:小仓库名字 第二个参数:箭头函数
let useTodoStore = defineStore('todo', () => {
let todos = ref([{ id: 1, title: '吃饭' }, { id: 2, title: '睡觉' }, { id: 3, title: '打豆豆' }]);
let arr = ref([1,2,3,4,5]);
const total = computed(() => {
return arr.value.reduce((prev, next) => {
return prev + next;
}, 0)
})
//务必要返回一个对象:属性与方法可以提供给组件使用
return {
todos,
arr,
total,
updateTodo() {
todos.value.push({ id: 4, title: '组合式API方法' });
}
}
});
export default useTodoStore;
组件中引入使用:
<template>
<div class="child1">
<p @click="updateTodo">{{ todoStore.arr }}{{todoStore.total}}</p>
</div>
</template>
<script setup lang="ts">
//引入组合式API函数仓库
import useTodoStore from "../../store/modules/todo";
let todoStore = useTodoStore();
//点击p段落去修改仓库的数据
const updateTodo = () => {
todoStore.updateTodo();
};
</script>
<style scoped>
.child1 {
width: 200px;
height: 200px;
background: hotpink;
}
</style>
9、插槽
插槽:默认插槽、具名插槽、作用域插槽
作用域插槽:就是可以传递数据的插槽,子组件可以将数据回传给父组件,父组件可以决定这些回传的数据是以何种结构或者外观在子组件内部
去展示!!!
父组件:
<template>
<div>
<h1>slot</h1>
<!-- 作用域插槽 -->
<Test1 :todos="todos">
<template v-slot="{ $row, $index }">
<p :style="{ color: $row.done ? 'green' : 'red' }">
{{ $row.title }}--{{ $index }}
</p>
</template>
</Test1>
<Test>
<!-- 默认插槽 -->
<div>
<pre>大江东去浪淘尽,千古分流人物</pre>
</div>
<!-- 具名插槽填充a v-slot:指令可以简化为# -->
<template #a>
<div>我是填充具名插槽a位置结构</div>
</template>
<!-- 具名插槽填充b -->
<template #b>
<div>我是填充具名插槽b位置结构</div>
</template>
</Test>
</div>
</template>
<script setup lang="ts">
import Test from "./Test.vue";
import Test1 from "./Test1.vue";
//插槽:默认插槽、具名插槽、作用域插槽
//作用域插槽:就是可以传递数据的插槽,子组件可以将数据回传给父组件,父组件可以决定这些回传的
//数据是以何种结构或者外观在子组件内部去展示!!!
import { ref } from "vue";
//todos数据
let todos = ref([
{ id: 1, title: "吃饭", done: true },
{ id: 2, title: "睡觉", done: false },
{ id: 3, title: "打豆豆", done: true },
{ id: 4, title: "打游戏", done: false },
]);
</script>
<style scoped>
</style>
默认插槽-具名插槽Test:
<template>
<div class="box">
<h1>我是子组件默认插槽</h1>
<!-- 默认插槽 -->
<slot></slot>
<h1>我是子组件默认插槽</h1>
<h1>具名插槽填充数据</h1>
<slot name="a"></slot>
<h1>具名插槽填充数据</h1>
<h1>具名插槽填充数据</h1>
<slot name="b"></slot>
<h1>具名插槽填充数据</h1>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped>
.box {
width: 100vw;
height: 500px;
background: skyblue;
}
</style>
作用域插槽Test1:
<template>
<div class="box">
<h1>作用域插槽</h1>
<ul>
<li v-for="(item, index) in todos" :key="item.id">
<!--作用域插槽:可以讲数据回传给父组件-->
<slot :$row="item" :$index="index"></slot>
</li>
</ul>
</div>
</template>
<script setup lang="ts">
//通过props接受父组件传递数据
defineProps(["todos"]);
</script>
<style scoped>
.box {
width: 100vw;
height: 400px;
background: skyblue;
}
</style>