📝 VUE3组件通信
1️⃣ props父子组件通信
props特性:
- **直接修改props:**Vue中的props是单向数据流,父组件传递给子组件的props应该被视为只读数据。不可以直接修改props中的数据,会导致应用的数据流变得难以追踪和理解。如果需要在子组件中修改传递的值,应该将其复制到组件内部的data或computed属性中进行修改
- **非响应式数据:**当props是基本类型(例如字符串、数字、布尔值等)时,修改其值不会影响到父组件中的数据。但如果props是对象或数组等引用类型数据,直接在子组件中修改这些引用类型的值会影响到父组件的数据,这可能导致意外的行为和bug。最好的做法是避免在子组件中直接修改这些引用类型的props,而是通过触发事件或调用父组件传递的函数来修改
- **命名冲突:**当命名多个props时,要注意避免命名冲突。尤其是在大型应用中,可能存在多个组件嵌套的情况,如果不注意命名规范,可能会导致props命名重复,造成混淆和错误。
- 合理使用v-model:在子组件中使用**
v-model
时,要确保v-model
的值是通过model
选项或名为value
的prop传递,并且要正确处理input
事件。在自定义组件中正确实现v-model
**功能可以提高组件的可重用性和易用性。 - 传递大量数据:不应该通过props传递大量的数据,特别是当这些数据需要在多个组件之间传递时。这样做可能会导致性能下降和难以维护的代码。大批量的数据可以使用状态管理的方式进行传递,后面会总结到pinia状态管理方案来替代Vuex
props组件通信代码示例:
父组件:parentComponents.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>
子组件:childComponents.vue**(defineProps方法是vue3提供的方法,直接使用不需要引入)**
<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方法去接受父组件传递过来的数据
let props = defineProps(['info','money']); //数组|对象写法都可以
//按钮点击的回调
const updateProps = ()=>{
console.log(props.info)
}
</script>
2️⃣ 自定义事件传值
自定义事件注意事项:
- 使用适当的命名规范:确保事件名称清晰明了,并且不会与现有的DOM事件名称冲突。
- 避免命名冲突:如果应用中有多个事件,确保它们的命名不会产生冲突。
- 组件解耦:自定义事件适用于解耦组件,不要过度使用,过度使用它们可能导致代码难以理解和维护。
修饰符操作
- .once 修饰符:可以使用
.once
修饰符,确保事件只被触发一次。 - .stop 修饰符:阻止事件继续传播到父元素。
- .prevent 修饰符:调用事件时调用
event.preventDefault()
,防止默认行为。 - .capture 修饰符:事件捕获阶段触发处理程序。
自定义事件传值代码示例:
父组件:parentComponents.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事件
-->
<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:object) => {
//event即为事件对象
console.log(event);
};
//事件回调--2
const handler1 = (a:number, b:number, c:number, $event:object) => {
console.log(a, b, c, $event);
};
//事件回调---3
const handler2 = () => {
console.log(123);
};
//事件回调---4
const handler3 = (param1:any, param2:any) => {
console.log(param1, param2);
};
//事件回调--5
const handler4 = (param1:any, param2:any) => {
console.log(param1, param2);
};
</script>
子组件1:Event1.vue**(此组件可直接执行父组件方法,未传值)**
<template>
<div class="son">
<p>我是子组件1</p>
<button>点击我也执行</button>
</div>
</template>
<script setup lang="ts">
</script>
子组件2: Event2.vue (defineEmits和props中的defineProps一样的性质,都是官方提供的方法,可不用引入直接使用)
<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>
3️⃣ 全局事件总线$bus
$bus封装与使用方法
$bus需要借助一个mitt的npm官方库,首先在项目中引入mitt,mitt官网,随后在项目中使用mitt方法创建一个$bus对象并挂载到vue实例:
$bus封装代码
//引入mitt插件:mitt一个方法,方法执行会返回bus对象
import mitt from 'mitt';
const $bus = mitt();
export default $bus;
$bus组件使用方法代码示例
父组件:parentComponents.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>
子组件1: 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>
子组件2: Child.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>
4️⃣ 使用v-model进行父子组件通信
v-model传值注意事项
- 正确实现v-model:在子组件中,要接受
value
和emit('update:modelValue', newValue)
来正确实现v-model
功能。 - 维护v-model的值:确保在子组件内部修改值时,不要直接修改
props
的值。应该在组件内部通过emit
触发update:modelValue
事件来通知父组件修改值。 - v-model修饰符:
v-model
支持一些修饰符,如.lazy
和.trim
,它们可以改变数据同步的时机或者处理输入的值。在需要的情况下可以使用这些修饰符。
v-model传值代码示例:
父组件:parentComponents.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: number) => {
//将来接受子组件传递过来的数据
money.value = num;
};
//父亲的数据
let pageNo = ref(1);
let pageSize = ref(3);
</script>
子组件1: 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>
子组件2:Child1.vue**(以分页参数为例,演示通过v-model传递多个数据)**
<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>
5️⃣ $attrs组件通信
$attrs使用注意事项
- 未声明为 Prop 的属性:
$attrs
包含的是父组件传递给子组件的但子组件没有声明为 prop 的所有属性。这些属性在子组件中可以被访问到,但是子组件并未明确声明它们,因此要格外小心对这些属性的使用和处理。 - 属性验证和处理: 子组件可以使用
$attrs
获取到父组件传递的属性,但这些属性并没有经过子组件的验证。因此,在使用这些属性之前,最好进行必要的验证、类型检查和处理,以确保数据的正确性和安全性。 - 避免冲突: 如果子组件的 prop 和
$attrs
中的属性名相同,Vue 3 的默认行为是优先使用 prop 中的属性。这可能会导致命名冲突或意外行为。因此,在设计组件时,要注意避免使用相同的属性名作为 prop 和$attrs
中的未声明 prop。 - 透传事件监听器: Vue 3 中,使用
v-on="$listeners"
可以将父组件的事件监听器传递给子组件。这在需要在子组件中监听相同事件时非常有用。这些事件监听器也可以在子组件中通过$attrs
访问到。 - 限制传递属性: 如果需要限制
$attrs
中的属性,可以通过设置inheritAttrs: false
来阻止未声明的属性传递给子组件。然后,通过v-bind="$attrs"
手动传递属性到子组件。 - 文档和注释: 在子组件中,最好通过文档、注释或者明确的命名,清楚地指出哪些属性来自于
$attrs
,以便其他开发者更好地理解和维护代码。
$attrs使用代码示例
父组件:parentComponents.vue (这种通信方式提到了一种V3自带API useAttrs 子组件代码对其有所解释)
<template>
<div>
<h1>useAttrs</h1>
<el-button type="primary" size="small" :icon="Edit"></el-button>
<!-- 自定义组件 -->
<HintButton
type="primary"
size="small"
:icon="Edit"
title="编辑按钮"
@click="handler"
@xxx="handler"></HintButton>
</div>
</template>
<script setup lang="ts">
//vue3框架提供一个方法useAttrs方法,它可以获取组件身上的属性与事件!!!
import HintButton from "./HintButton.vue";
//按钮点击的回调
const handler = () => {
alert(12306);
};
</script>
子组件: HintButton.vue
<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>
6️⃣ 使用ref进行父子组件通信
ref使用规范
- Vue 3 Composition API:
ref
是 Vue 3 Composition API 的一部分,它允许你在组件中创建响应式的数据。通过ref
创建的变量是响应式的,可以在模板和组件逻辑中使用。 - 创建和访问
ref
变量:要创建一个ref
变量,你可以使用ref()
函数。为了访问ref
变量的值,你需要使用.value
。在模板中使用ref
变量时,不需要.value
。 - 注意
ref
变量的响应性:ref
变量本身是响应式的,但是如果将ref
直接赋值给普通的变量或对象,那么对普通变量的更改不会触发组件的重新渲染。如果需要在模板中更新视图,应该使用ref
的.value
进行更改。 - 异步更新
ref
:如果要在异步操作中更改ref
的值,需要注意 Vue 3 中提供的nextTick
方法来确保更新发生在视图更新周期之后。 - 避免直接修改
ref
对象:直接修改ref
对象可能导致一些意料之外的问题。应该通过.value
进行修改和访问。
ref通信代码示例
父组件:parentComponents.vue
- ref:可以获取真实的DOM节点,可以获取到子组件实例VC
- $parent:可以在子组件内部获取到父组件的实例
<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">
//引入子组件
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>
子组件1: Son.vue**(defineExpose)**
- 组件内部数据对外关闭的,别人不能访问
- 如果想让外部访问需要通过defineExpose方法对外暴露
<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>
子组件2: Daughter.vue
<template>
<div class="dau">
<h1>我是闺女曹杰{{money}}</h1>
<button @click="handler($parent)">点击我爸爸给我10000元</button>
</div>
</template>
<script setup lang="ts">
import {ref} from 'vue';
//闺女钱数
let money = ref(999999);
//闺女按钮点击回调
const handler = ($parent:any)=>{
money.value+=10000;
$parent.money-=10000;
}
</script>
7️⃣ Provide与Inject跨层组件通信
Provide与Inject说明
Provide:(provide
用于在父级组件中提供数据,使其在子组件中可用。)
- 提供数据:通过在父组件中使用
provide
提供数据给子组件 - 全局 Provide:可以在根组件中使用
provide
来提供全局数据,让所有后代组件都能够访问。
Inject:(inject
用于从父级组件或祖先组件中注入数据)
- 注入数据:在子组件中使用
inject
从父组件或祖先组件中注入数据。 - 提供默认值:可以在
inject
中提供默认值,以防注入的数据未在父组件中提供。
Provide与Inject规范:
- 慎用全局
provide
:避免滥用全局provide
,因为它可能会导致组件之间的紧密耦合。更好的做法是在适当的情况下,仅在需要时在局部范围内使用provide
和inject
。 - 适度使用:
provide
和inject
应该用于在特定场景下解决组件通信的问题,而不是被用作全局状态管理器。对于跨组件通信,Vuex 或其他专门的状态管理工具可能更合适。 - 文档和约定:为了维护代码的可读性和可维护性,应该在代码中明确地注明哪些数据通过
provide
提供,以及哪些数据通过inject
注入。 - 依赖注入的注意事项:当使用
inject
时,确保了解数据的来源。它不仅可以从直接的父组件中注入,还可以从更上层的祖先组件中提供的数据。
Provide与Inject代码示例:
父组件:ParentComponents.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>
子组件(二级组件): Child.vue
<template>
<div class="child">
<h1>我是子组件1</h1>
<Child></Child>
</div>
</template>
<script setup lang="ts">
import Child from './GrandChild.vue';
</script>
重子组件(三级组件): 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>
8️⃣ Pinia状态管理方案 Pinia国内官网
9️⃣ 使用插槽进行组件通信
插槽解释:
插槽是 Vue 中一种非常强大的机制,可用于实现组件间的通信和数据传递。通过插槽,父组件可以向子组件传递内容,而子组件可以决定如何展示这些内容。以下是使用插槽进行组件通信的说明以及一些规范:
插槽基本用法
- 父组件向子组件传递内容:父组件中的内容可以被传递到子组件中,子组件可以通过插槽(slot)接收并渲染这些内容。
<!-- ParentComponent.vue -->
<template>
<ChildComponent>
<!-- 父组件中的内容通过插槽传递给子组件 -->
<p>Content passed to child component</p>
</ChildComponent>
</template>
<!-- ChildComponent.vue -->
<template>
<div>
<!-- 子组件中使用插槽渲染父组件传递的内容 -->
<slot></slot>
</div>
</template>
- 具名插槽:除了默认插槽外,还可以使用具名插槽来传递特定名称的内容。
<!-- ParentComponent.vue -->
<template>
<ChildComponent>
<!-- 使用具名插槽传递不同名称的内容 -->
<template v-slot:header>
<h1>Header content for child component</h1>
</template>
<template v-slot:footer>
<p>Footer content for child component</p>
</template>
</ChildComponent>
</template>
<!-- ChildComponent.vue -->
<template>
<div>
<header>
<!-- 渲染具名插槽的内容 -->
<slot name="header"></slot>
</header>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
规范和最佳实践
- 语义化插槽名称:为插槽使用有意义的名称,使得组件使用者更容易理解和操作。这有助于代码的可维护性。
- 适当使用具名插槽:对于复杂的组件,合理使用具名插槽可以更清晰地分离内容,提高组件的可复用性。
- 提供默认内容:为插槽提供默认内容,以防止未提供内容时组件渲染出错
<!-- ChildComponent.vue -->
<template>
<div>
<!-- 提供默认内容 -->
<slot>
<p>Default content if nothing provided</p>
</slot>
</div>
</template>
- 插槽作用域:插槽内容可能需要访问子组件的数据或方法。在子组件中使用作用域插槽,将数据和方法传递给插槽内容。
<!-- ChildComponent.vue -->
<template>
<div>
<slot :data="myData" :method="myMethod"></slot>
</div>
</template>
<script>
export default {
data() {
return {
myData: 'Some data',
};
},
methods: {
myMethod() {
// Some method logic
},
},
};
</script>
<!-- ParentComponent.vue -->
<template>
<ChildComponent>
<!-- 插槽作用域 -->
<template v-slot="{ data, method }">
<p>{{ data }}</p>
<button @click="method">Click me</button>
</template>
</ChildComponent>
</template>
🤗 总结归纳
本文章篇幅较长,方法较多,常用的无非久那几种,要看情况根据自身业务需求选择更适合自己的实现思路和方法,第八条Pinia严格说不是组件通信,而是同Vuex一样的,状态管理库,我会再更新另一篇文章单独说Pinia的使用和Vuex两者之间的区别