Vue3 组件通信的5种方式详解
该博客内容是根据尚硅谷 贾成豪老师的 Vue 项目硅谷甄选 视频整理而成。
Vue2 组件通信方式介绍
- props: 可以实现父子组件、子父组件、甚至兄弟组件通信。
- 自定义事件:可以实现子父组件通信。
- 全局事件总线
$bus
:可以实现任意组件通信。 - pubsub:发布订阅模式实现任意组件通信。
- vuex:集中式状态管理容器,实现任意组件通信。
- ref:父组件获取子组件实例 VC ,获取子组件的响应式数据以及方法。
- slot:插槽(默认插槽、具名插槽、作用域插槽)实现父子组件通信。
- …
props
props 可以实现父子组件通信,在 Vue3 中我们可以通过 defineProps 获取父组件传递的数据。并且在组件内部可以不引入 defineProps 而直接使用 defineProps 方法。
<!-- 父组件 -->
<!-- src/views/01_props/PropsTest.vue -->
<template>
<div class="box">
<h1>props:我是父组件曹操</h1>
<hr />
<Child info="我是曹操" :money="money"></Child>
</div>
</template>
<script setup lang="ts">
//props:可以实现父子组件通信,props数据还是只读的!!!
import Child from "./Child.vue";
import { ref } from "vue";
let money = ref(10000);
</script>
<style scoped>
.box {
width: 100vw;
height: 400px;
background: yellowgreen;
}
</style>
<!-- 子组件 -->
<!-- src/views/01_props/Child.vue -->
<template>
<div class="son">
<h1>我是子组件:曹植</h1>
<p>{{props.info}}</p>
<p>{{props.money}}</p>
<!--props可以省略前面的名字--->
<p>{{info}}</p>
<p>{{money}}</p>
<button @click="updateProps">修改props数据</button>
</div>
</template>
<script setup lang="ts">
//需要使用到defineProps方法去接受父组件传递过来的数据
//defineProps是Vue3提供方法,不需要引入直接使用
let props = defineProps(["info", "money"]); //数组|对象写法都可以
//按钮点击的回调
const updateProps = () => {
// props.money+=10; // props:只读的
console.log(props.info);
};
</script>
<style scoped>
.son {
width: 400px;
height: 200px;
background: hotpink;
}
</style>
props 是只读的无法在子组件进行修改
在子组件 template 中 props 可以省略前面的名字,但在 script 中 props 不可省略
自定义事件
在 Vue 框架中事件分为两种:
- 原生的 DOM 事件
- 自定义事件
原生 DOM 事件可以让用户与网页进行交互,比如 click、dbclick、change、mouseenter、mouseleave…
自定义事件可以实现子组件给父组件传递数据
原生 DOM 事件
<pre @click="handler">
我是祖国的老花骨朵
</pre>
当前代码给 pre 标签绑定原生 DOM 事件点击事件,默认会给事件回调注入 event 事件对象。当点击事件想注入多个参数可以按照下图操作。但是切记注入的事件对象务必叫做 $event
<div @click="handler(1,2,3,$event)">我要传递多个参数</div>
在 Vue3 框架 click、dbclick、change(这类原生 DOM 事件),不管是在标签、自定义标签上(组件标签)都是原生 DOM 事件。
自定义事件
自定义事件可以实现子组件给父组件传递数据,在项目中还是比较常用的。
代码展示
<!-- 父组件 -->
<!-- src/views/02_custom-event/EventTest.vue -->
<template>
<div>
<h1>事件</h1>
<!-- 原生DOM事件 -->
<pre @click="handler">
大江东去浪淘尽,千古分流人物
</pre>
<button @click="handler1(1,2,3,$event)">点击我传递多个参数</button>
<hr />
<!--
vue2框架当中:这种写法自定义事件,可以通过.native修饰符变为原生DOM事件
vue3框架下面写法其实即为原生DOM事件
vue3:原生的DOM事件不管是放在标签身上、组件标签身上都是原生DOM事件
-->
<!--
实际上事件绑定在了子组件的根节点上
当点击了 根节点的按钮 会根据事件委派也会触发 handler2 事件
-->
<Event1 @click="handler2"></Event1>
<hr />
<!-- 绑定自定义事件xxx:实现子组件给父组件传递数据 -->
<Event2 @xxx="handler3" @click="handler4"></Event2>
</div>
</template>
<script setup lang="ts">
//引入子组件
import Event1 from "./Event1.vue";
//引入子组件
import Event2 from "./Event2.vue";
//事件回调--1
const handler = (event) => {
//event即为事件对象
console.log(event);
};
//事件回调--2
const handler1 = (a, b, c, $event) => {
console.log(a, b, c, $event);
};
//事件回调---3
const handler2 = () => {
console.log(123);
};
//事件回调---4
const handler3 = (param1, param2) => {
console.log(param1, param2);
};
//事件回调--5
const handler4 = (param1, param2) => {
console.log(param1, param2);
};
</script>
<style scoped></style>
<!-- 子组件1 -->
<!-- src/views/02_custom-event/Event1.vue -->
<template>
<div class="son">
<p>我是子组件1</p>
<button>点击我也执行</button>
</div>
</template>
<script setup lang="ts"></script>
<style scoped>
.son {
width: 400px;
height: 200px;
background: skyblue;
}
</style>
<!-- 子组件2 -->
<!-- src/views/02_custom-event/Event2.vue -->
<template>
<div class="child">
<p>我是子组件2</p>
<button @click="handler">点击我触发自定义事件xxx</button>
<button @click="$emit('click','AK47','J20')">
点击我触发自定义事件click
</button>
</div>
</template>
<script setup lang="ts">
//利用defineEmits方法返回函数触发自定义事件
//defineEmits方法不需要引入直接使用
let $emit = defineEmits(["xxx", "click"]);
//按钮点击回调
const handler = () => {
//第一个参数:事件类型 第二个|三个|N参数即为注入数据
$emit("xxx", "东风导弹", "航母");
};
</script>
<style scoped>
.child {
width: 400px;
height: 200px;
background: pink;
}
</style>
将原生 DOM 事件使用 emit 接收,此时的原生 DOM 事件就是自定义事件了
如:在子组件 2 中使用了 defineEmits 接受 click
全局事件总线
全局事件总线可以实现任意组件通信,在 Vue2 中可以根据 VM 与 VC 关系推出全局事件总线。但是在 Vue3 中没有 Vue 构造函数,也就没有 Vue.prototype 以及组合 API 写法、没有 this ,那么在 Vue3 想实现全局事件的总线功能就有点不现实了,如果想在 Vue3 中使用全局事件总线功能可以使用插件 mitt 实现。
// src/bus/index.ts
//引入mitt插件:mitt一个方法,方法执行会返回bus对象
import mitt from "mitt";
const $bus = mitt();
export default $bus;
<!-- 父组件 -->
<!-- src/views/03_event-bus/EventBusTest.vue -->
<template>
<div class="box">
<h1>全局事件总线$bus</h1>
<hr />
<div class="container">
<Child1></Child1>
<Child2></Child2>
</div>
</div>
</template>
<script setup lang="ts">
//引入子组件
import Child1 from "./Child1.vue";
import Child2 from "./Child2.vue";
</script>
<style scoped>
.box {
width: 100vw;
height: 400px;
background: yellowgreen;
}
.container {
display: flex;
justify-content: space-between;
}
</style>
<!-- 子组件1 -->
<!-- src/views/03_event-bus/Child1.vue -->
<template>
<div class="child1">
<h3>我是子组件1:曹植</h3>
</div>
</template>
<script setup lang="ts">
import $bus from "../../bus";
//组合式API函数
import { onMounted } from "vue";
//组件挂载完毕的时候,当前组件绑定一个事件,接受将来兄弟组件传递的数据
onMounted(() => {
//第一个参数:即为事件类型 第二个参数:即为事件回调
$bus.on("car", (car) => {
console.log(car);
});
});
</script>
<style scoped>
.child1 {
width: 300px;
height: 300px;
background: hotpink;
}
</style>
<!-- 子组件2 -->
<!-- src/views/03_event-bus/Child2.vue -->
<template>
<div class="child2">
<h2>我是子组件2:曹丕</h2>
<button @click="handler">点击我给兄弟送一台法拉利</button>
</div>
</template>
<script setup lang="ts">
//引入$bus对象
import $bus from "../../bus";
//点击按钮回调
const handler = () => {
$bus.emit("car", { car: "法拉利" });
};
</script>
<style scoped>
.child2 {
width: 300px;
height: 300px;
background: skyblue;
}
</style>
v-model
<!-- 父组件 -->
<!-- src/views/04_v-model/ModelTest.vue -->
<template>
<div>
<h1>v-model:钱数{{ money }}{{pageNo}}{{pageSize}}</h1>
<input type="text" v-model="info" />
<hr />
<!-- props:父亲给儿子数据 -->
<!-- <Child :modelValue="money" @update:modelValue="handler"></Child> -->
<!--
v-model组件身上使用
第一:相当有给子组件传递props[modelValue] = 10000
第二:相当于给子组件绑定自定义事件update:modelValue
-->
<Child v-model="money"></Child>
<hr />
<Child1 v-model:pageNo="pageNo" v-model:pageSize="pageSize"></Child1>
</div>
</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 info = ref("");
//父组件的数据钱数
let money = ref(10000);
//自定义事件的回调
const handler = (num) => {
//将来接受子组件传递过来的数据
money.value = num;
};
//父亲的数据
let pageNo = ref(1);
let pageSize = ref(3);
</script>
<style scoped></style>
<!-- 子组件1 -->
<!-- src/views/04_v-model/Child.vue -->
<template>
<div class="child">
<h3>钱数:{{ modelValue }}</h3>
<button @click="handler">父子组件数据同步</button>
</div>
</template>
<script setup lang="ts">
//接受props
let props = defineProps(["modelValue"]);
let $emit = defineEmits(["update:modelValue"]);
//子组件内部按钮的点击回调
const handler = () => {
//触发自定义事件
$emit("update:modelValue", props.modelValue + 1000);
};
</script>
<style scoped>
.child {
width: 600px;
height: 300px;
background: skyblue;
}
</style>
<!-- 子组件2 -->
<!-- src/views/04_v-model/Child1.vue -->
<template>
<div class="child2">
<h1>同时绑定多个v-model</h1>
<button @click="handler">pageNo{{ pageNo }}</button>
<button @click="$emit('update:pageSize', pageSize + 4)">
pageSize{{ pageSize }}
</button>
</div>
</template>
<script setup lang="ts">
let props = defineProps(["pageNo", "pageSize"]);
let $emit = defineEmits(["update:pageNo", "update:pageSize"]);
//第一个按钮的事件回调
const handler = () => {
$emit("update:pageNo", props.pageNo + 3);
};
</script>
<style scoped>
.child2 {
width: 300px;
height: 300px;
background: hotpink;
}
</style>
useAttrs
代码实现功能描述:封装 element-plus 按钮组件,添加标题提示功能。
引入 element-plus
// src/main.ts
// 引入实例化上下文的api方法createApp
import { createApp } from "vue";
// 引入element ui
import ElementPlus from "element-plus";
// 引入element ui的样式
import "element-plus/dist/index.css";
// 引入App组件
import App from "./App.vue";
// 引入路由器
import router from "./router";
//引入仓库
import store from "./store";
// 创建app
const app = createApp(App);
app.use(store);
app.use(router); // 注册路由器
app.use(ElementPlus); // 使用element-ui
// 挂载
app.mount("#app");
<!-- 父组件 -->
<!-- src/views/05_attrs-listeners/AttrsListenersTest.vue -->
<template>
<div :title="title">
<el-button :="$attrs"></el-button>
<!-- <h1 v-bind="{a:1,b:2}">123</h1> -->
<!-- <h1 :="{a:1,b:2}">123</h1> -->
</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);
// $attrs.onXxx()
</script>
<style scoped></style>
<!-- 子组件 -->
<!-- src/views/05_attrs-listeners/HintButton.vue -->
<template>
<div :title="title">
<el-button :="$attrs"></el-button>
<!-- <h1 v-bind="{a:1,b:2}">123</h1> -->
<!-- <h1 :="{a:1,b:2}">123</h1> -->
</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);
// $attrs.onXxx()
</script>
<style scoped></style>
props 的优先级比 useAttrs 高
props 与 useAttrs 方法都可以获取父组件传递过来的属性与属性值,但是 props 接收了 useAttrs 方法就获取不到了
ref 和 $parent
ref:可以获取真实的 DOM 节点,可以获取到子组件实例 VC
$parent
:可以在子组件内部获取到父组件的实例
<!-- 父组件 -->
<!-- src/views/06_ref-children-parent/RefChildrenParentTest.vue -->
<template>
<div class="box">
<h1>我是父亲曹操:{{money}}</h1>
<button @click="handler">找我的儿子曹植借10元</button>
<hr />
<Son ref="son"></Son>
<hr />
<Dau></Dau>
</div>
</template>
<script setup lang="ts">
//ref:可以获取真实的DOM节点,可以获取到子组件实例VC
//$parent:可以在子组件内部获取到父组件的实例
//引入子组件
import Son from "./Son.vue";
import Dau from "./Daughter.vue";
import { ref } from "vue";
//父组件钱数
let money = ref(100000000);
//获取子组件的实例
let son = ref();
//父组件内部按钮点击回调
const handler = () => {
money.value += 10;
//儿子钱数减去10
son.value.money -= 10;
son.value.fly();
};
//对外暴露
defineExpose({
money,
});
</script>
<style scoped>
.box {
width: 100vw;
height: 500px;
background: skyblue;
}
</style>
<!-- 子组件 -->
<!-- src/views/06_ref-children-parent/Son.vue -->
<template>
<div class="son">
<h3>我是子组件:曹植{{money}}</h3>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
//儿子钱数
let money = ref(666);
const fly = () => {
console.log("我可以飞");
};
//组件内部数据对外关闭的,别人不能访问
//如果想让外部访问需要通过defineExpose方法对外暴露
defineExpose({
money,
fly,
});
</script>
<style scoped>
.son {
width: 300px;
height: 200px;
background: cyan;
}
</style>
<!-- 子组件2 -->
<!-- src/views/06_ref-children-parent/Daughter.vue -->
<template>
<div class="dau">
<h1>我是闺女曹杰{{money}}</h1>
<!-- 必须叫做 $parent -->
<button @click="handler($parent)">点击我爸爸给我10000元</button>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
//闺女钱数
let money = ref(999999);
//闺女按钮点击回调
const handler = ($parent) => {
money.value += 10000;
$parent.money -= 10000;
};
</script>
<style scoped>
.dau {
width: 300px;
height: 300px;
background: hotpink;
}
</style>
provide 和 inject
Vue3 提供 provide (提供)与 inject (注入),可以实现隔辈组件传递数据
<!-- 祖组件 -->
<!-- src/views/07_provide-inject/ProvideInjectTest.vue -->
<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>
<!-- 子组件 -->
<!-- src/views/07_provide-inject/Child.vue -->
<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>
<!-- 孙组件 -->
<!-- src/views/07_provide-inject/GrandChild.vue -->
<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>
pinia
缺点:存储的数据并非持久化
vuex:集中式管理状态容器,可以实现任意组件之间通信
核心概念: state、mutations、actions、getters、modules
pinia:集中式管理状态容器,可以实现任意组件之间通信
核心概念: state、actions、getters
写法:选择器 API、组合式 API
配置使用 pinia
// src/store/index.ts
//创建大仓库
import { createPinia } from 'pinia';
//createPinia方法可以用于创建大仓库
let store = createPinia();
//对外暴露,安装仓库
export default store;
// src/main.ts
import { createApp } from 'vue'
...
//引入仓库
import store from './store'
// 创建app
const app = createApp(App)
app.use(store)
...
选择式 API
// src/store/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;
组合式 API
// src/store/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;
代码展示
<!-- 父组件 -->
<!-- src/views/08_pinia/index.vue -->
<template>
<div class="box">
<h1>pinia</h1>
<div class="container">
<Child></Child>
<Child1></Child1>
</div>
</div>
</template>
<script setup lang="ts">
import Child from "./Child.vue";
import Child1 from "./Child1.vue";
</script>
<style scoped>
.box {
width: 600px;
height: 400px;
background: skyblue;
}
.container {
display: flex;
}
</style>
<!-- 子组件1 -->
<!-- src/views/08_pinia/Child.vue -->
<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:111
// })
//仓库调用自身的方法去修改仓库的数据
infoStore.updateNum(66, 77);
};
</script>
<style scoped>
.child {
width: 200px;
height: 200px;
background: yellowgreen;
}
</style>
<!-- 子组件2 -->
<!-- src/views/08_pinia/Child1.vue -->
<template>
<div class="child1">
{{ infoStore.count }}
<p @click="updateTodo">{{ todoStore.arr }}{{todoStore.total}}</p>
</div>
</template>
<script setup lang="ts">
import useInfoStore from "../../store/modules/info";
//获取小仓库对象
let infoStore = useInfoStore();
//引入组合式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>
slot
插槽:默认插槽、具名插槽、作用域插槽
作用域插槽:就是可以传递数据的插槽,子组件可以将数据回传给父组件,父组件可以决定这些回传的数据是以何种结构或者外观在子组件内部去展示。
<!-- src/views/09_slot/SlotTest.vue -->
<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 -->
<template #a>
<div>我是填充具名插槽a位置结构</div>
</template>
<!-- 具名插槽填充b v-slot指令可以简化为# -->
<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>
<!-- src/views/09_slot/Test.vue -->
<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>
<!-- src/views/09_slot/Test1.vue -->
<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>