Vue 3 组件通信的常用方式
Vue 3 是一个用于构建用户界面的渐进式 JavaScript 框架。组件是 Vue 的核心概念之一,它们可以封装可复用的代码片段,并且可以在整个应用中重复使用。为了实现组件之间的交互,Vue 提供了多种通信方式。
一、props/$emit
props是使用频率最高的一种通信方式,常用于 :父 ↔ 子。
- 若 父传子:属性值是非函数。
- 若 子传父:属性值是函数。
特点:
- 单向数据流:父组件的数据变化会流向子组件,但不会反过来。这有助于保持状态管理的清晰性和可预测性。
- 静态
props:组件实例化时就被确定,并且不会随时间变化的属性值,动态props:其值会根据父组件的数据状态而变化的属性
<my-component title="Static Title"></my-component>
<my-component :title="dynamicTitle"></my-component>
- 推荐使用驼峰命名法
父组件:
<template>
<div class="father" >
<h3>父组件,</h3>
<h4>我的车:{{ car }}</h4>
<h4>儿子给的玩具:{{ toy }}</h4>
<Child :car="car" handsome="帅气" :getToy="getToy" />
</div>
</template>
<script setup>
import Child from './Child.vue'
import { ref } from "vue";
// 数据
const car = ref('奔驰')
const toy = ref()
// 方法
function getToy(value){
toy.value = value
}
</script>
子组件:
<template>
<div class="child">
<h3>子组件</h3>
<h4>我的玩具:{{ toy }}</h4>
<h4>父给我的车:{{ car }}</h4>
<h4>父给我的外貌:{{ handsome }}</h4>
<button @click="invokeParent">玩具给父亲</button>
</div>
</template>
<script setup>
import { ref } from "vue";
const toy = ref('奥特曼')
let props = defineProps(['car','handsome','getToy'])
function invokeParent() {
props.getToy(toy.value)
}
</script>
$emit自定义事件常用于:子 => 父。
-
子组件通过
$emit触发自定义事件来通知父组件状态变化。 -
原生事件:
- 事件名是特定的(
click、mosueenter等等) - 事件对象
$event: 是包含事件相关信息的对象(pageX、pageY、target、keyCode)
- 事件名是特定的(
-
自定义事件:
- 事件名是任意名称
- 事件对象
$event: 是调用emit时所提供的数据,可以是任意类型!!!
父组件:
<template>
<div>
<h2>父组件</h2>
<p>来自子组件的消息:{{ messageFromChild }}</p>
<!-- 父子组件之间约定:子组件调用child-message事件传递消息给父组件,父组件监听child-message事件处理消息。 -->
<ChildComponent @child-message="handleChildMessage" />
</div>
</template>
<script setup>
// @ 事件 不是js的内置事件 是自定义事件
// 父子组件通信
import { ref } from 'vue'
import ChildComponent from './components/ChildComponent.vue'
// 自定义事件 child-message 父子组件之间的约定 子组件调用child-message事件 传递消息给父组件
const messageFromChild = ref('')
// 处理子组件的消息 handleChildMessage 事件处理函数
const handleChildMessage = (message) => {
console.log('父组件收到子组件的消息:', message)
messageFromChild.value = message
}
</script>
子组件:
<template>
<div>
<h1>{{ title }}</h1>
<button @click="sendMessageToParent">发送消息给父组件</button>
</div>
</template>
<script setup>
import {
ref,
} from 'vue'
const childMessage = ref('Hello ,parent!')
// emits 触发 汇报
const emits = defineEmits(['child-message'])
const sendMessageToParent = () => {
emits('child-message', childMessage.value)
}
</script>
直接传递函数和使用
$emit触发事件在表面上看起来相似,因为两者都可以让子组件与父组件进行通信,然而,它们之间有几个关键的区别:
单向数据流
$emit: 遵循 Vue 的单向数据流原则。子组件通过触发事件通知父组件发生了什么,而不是直接调用父组件的方法。这使得数据流动清晰明了,便于理解和调试。- 直接传递函数: 子组件可以直接调用父组件提供的方法,这可能会模糊父子组件之间的界限,因为它打破了单向数据流的概念,使数据流变得不那么直观。
组件解耦
$emit: 使用$emit可以让子组件保持对父组件内部实现细节的无知。它只关心何时以及如何发出事件,而不关心父组件会怎样响应这些事件。这样可以提高组件的可复用性和独立性。- 直接传递函数: 直接传递函数的方式增加了父子组件间的耦合度。子组件需要知道父组件提供的具体函数,并且依赖于该函数的存在和签名。如果父组件中的函数逻辑发生变化,可能会影响到所有使用它的子组件。
代码清晰度
$emit: 有助于维持清晰的代码结构,特别是在大型项目中。它明确区分了“谁”(子组件)做了“什么”(触发了某个事件),以及“谁”(父组件)对此采取了行动。- 直接传递函数: 可能使代码逻辑变得不那么清晰,特别是当多个层级的组件相互调用时,可能会难以追踪数据流。
二、mitt
与消息订阅与发布(pubsub)功能类似,可以实现任意组件间通信。
mitt 是一个非常轻量级的事件发射器(event emitter)库,它没有依赖项,并且非常适合用于需要简单事件管理的场景。
- 安装
mitt
npm i mitt
- 新建文件:
src\utils\emitter.js
import mitt from 'mitt';
const emitter = mitt();
export default emitter;
-
mitt的 APImitt(): 创建一个新的事件发射器实例。on(type, handler): 注册一个事件监听器,当指定类型的事件被触发时调用handler函数。off(type, [handler]): 移除指定类型的事件监听器。如果不提供handler参数,则移除所有该类型的监听器。emit(type, [evt]): 触发指定类型的事件,传递给所有已注册的监听器。evt参数可以是任意类型的数据。
- 生命周期管理:确保在组件卸载时清理不再需要的事件监听器,以防止内存泄漏。
三、v-model
v-model实现 父↔子 之间相互通信。
前序知识 —— v-model的本质
```
<!-- 使用v-model指令 -->
<input type="text" v-model="userName">
<!-- v-model的本质是下面这行代码 -->
<input
type="text"
:value="userName"
@input="userName =$event.target.value"
>
```
如果在自定义组件上使用 v-model 而没有指定任何修饰符或额外配置,默认情况下它会:
- 将父组件的数据通过一个名为
modelValue的 prop 传递给子组件。 - 监听子组件发出的一个名为
update:modelValue的事件来更新父组件中的数据。
<!-- 组件标签上使用v-model指令 -->
<MyInput v-model="userName"/>
<!-- 组件标签上v-model的本质 -->
<MyInput :modelValue="userName" @update:model-value="userName = $event"/>
- 多
v-model支持:你可以通过修饰符的方式为同一个组件定义多个v-model绑定。
<MyInput v-model:abc="userName" v-model:xyz="password"/>
四、$attrs
$attrs用于实现当前组件的父组件,向当前组件的子组件通信(祖→孙)。
具体说明:$attrs是一个对象,包含所有父组件传入的标签属性。
注意:
$attrs会自动排除props中声明的属性(可以认为声明过的props被子组件自己“消费”了)
父组件:
<template>
<div class="father">
<h3>父组件</h3>
<Child :a="a" :b="b" :c="c" :d="d" 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)
let c = ref(3)
let d = ref(4)
function updateA(value){
a.value = value
}
</script>
子组件:
<template>
<div class="child">
<h3>子组件</h3>
<GrandChild v-bind="$attrs"/>
</div>
</template>
<script setup lang="ts" name="Child">
import GrandChild from './GrandChild.vue'
</script>
孙组件:
<template>
<div class="grand-child">
<h3>孙组件</h3>
<h4>a:{{ a }}</h4>
<h4>b:{{ b }}</h4>
<h4>c:{{ c }}</h4>
<h4>d:{{ d }}</h4>
<h4>x:{{ x }}</h4>
<h4>y:{{ y }}</h4>
<button @click="updateA(666)">点我更新A</button>
</div>
</template>
<script setup lang="ts" name="GrandChild">
defineProps(['a','b','c','d','x','y','updateA'])
</script>
五、refs/parent
概述:
- `$refs`用于 :**父→子。**
- `$parent`用于:**子→父。**
原理如下:
| 属性 | 说明 |
| --------- | --------------------------------- |
| `$refs` | 值为对象,包含所有被`ref`属性标识的`DOM`元素或组件实例。 |
| `$parent` | 值为对象,当前组件的父组件实例对象。
六、 provide/inject
概述:实现祖孙组件直接通信 具体使用:
- 在祖先组件中通过`provide`配置向后代组件提供数据
- 在后代组件中通过`inject`配置来声明接收数据
父组件
<script setup>
import Child from './Child.vue'
import { ref,reactive,provide } from "vue";
// 数据
let money = ref(100)
let car = reactive({
brand:'奔驰',
price:100
})
// 用于更新money的方法
function updateMoney(value:number){
money.value += value
}
// 提供数据
provide('moneyContext',{money,updateMoney})
provide('car',car)
</script>
孙组件
<script setup>
import { inject } from 'vue';
// 注入数据
//可以指定默认值
let {money,updateMoney} = inject('moneyContext',{money:0,updateMoney:(x:number)=>{}})
let car = inject('car')
七、slot
通过使用 slot,你可以将内容从父组件传递到子组件中指定的位置,这使得子组件可以在不暴露其内部实现细节的情况下,灵活地嵌入外部提供的内容。
Slot 的基本类型
- 默认插槽(Default Slot)
这是最简单的形式,当没有指定名称时,默认插槽会占据子组件中的 标签位置。
- 具名插槽(Named Slot)
当你需要多个不同的插槽时,可以使用具名插槽。每个具名插槽都有一个唯一的名称,以便父组件能够明确地向哪个插槽插入内容。
- 作用域插槽(Scoped Slot)
作用域插槽允许子组件向父组件传递数据。父组件可以通过模板语法访问这些数据,并据此渲染内容。实际上,这是父组件定义了如何展示子组件的数据。
父组件
<template>
<div>
<h2>我是父组件</h2>
<!-- 使用默认插槽 -->
<ChildComponent>
<p>这是默认插槽的内容。</p>
</ChildComponent>
<!-- 使用具名插槽 -->
<ChildComponent>
<template #header>
<p>这是头部内容。</p>
</template>
<template #default>
<p>这是默认插槽的内容。</p>
</template>
<template #footer>
<p>这是底部内容。</p>
</template>
</ChildComponent>
<!-- 使用作用域插槽 -->
<ChildComponent>
<template #scoped="{ user }">
<p>{{ user.name }} - {{ user.age }}</p>
</template>
</ChildComponent>
</div>
</template>
<script setup>
import ChildComponent from './ChildComponent.vue';
</script>
子组件
<template>
<div class="child-component">
<!-- 默认插槽 -->
<slot></slot>
<hr />
<!-- 具名插槽 -->
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot> <!-- 可以再次使用默认插槽 -->
</main>
<footer>
<slot name="footer"></slot>
</footer>
<hr />
<!-- 作用域插槽 -->
<slot name="scoped" :user="user"></slot>
</div>
</template>
<script setup>
// 定义要传递给作用域插槽的数据
const user = {
name: '张三',
age: 30,
};
</script>
八、pinia
之前写过pinia的基本使用,可以参考pinia入门,这里就不赘述了。