在 Vue 3 中,组件通信是一个关键的概念,它允许我们在组件之间传递数据和事件。
vue3 组件通信方式
-
父传子
props
v-model
$refs
- 默认插槽、具名插槽、动态插槽
-
子传父
props
v-model
$parent
- 自定义事件
- 作用域插槽
-
祖传孙、孙传祖
$attrs
provide
、inject
-
兄弟间、任意组件间
- Pinia
mitt
Vue3组件通信和Vue2的区别
- 移出事件总线,使用
mitt
代替。 vuex
换成了pinia
。- 把
.sync
优化到了v-model
里面了。 - 把
$listeners
所有的东西,合并到$attrs
中了。 $children
被砍掉了。
props
props
是使用频率最高的一种通信方式,常用于 :父 ↔ 子。
- 若 父传子 :属性值是非函数。
- 若 子传父 :属性值是函数 。
- 使用
props
实现父传子,需要父组件先传递给子组件一个函数,子组件调用父组件给的函数实现子传父。
- 使用
更多关于Props的知识点请查看vue3 Props的用法(父传子)
示例
父组件Father.vue
:
html
<template>
<div>
<h3>在父组件中</h3>
<p>父组件的房子:{{ house }}</p>
<p>父组件的车:{{ car}}</p>
<p>子组件传给父组件的生日礼物:{{ birthdayGift }}</p>
<ChildComponent :car="car" :giveBirthdayGift="getBirthdayGift" />
</div>
</template>
<script setup lang="ts" name="Father">
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
// 数据
const house = ref('独栋小别墅')
const car = ref('奔驰');
const birthdayGift = ref();
// 方法
function getBirthdayGift(value: string) {
birthdayGift.value = value;
}
</script>
子组件ChildComponent.vue
:
html
<template>
<div>
<h3>子组件</h3>
<h4>子给父 买的生日礼物:{{ birthdayGift }}</h4>
<h4>父给子的车:{{ car }}</h4>
<button @click="giveBirthdayGift(birthdayGift )">把生日礼物送给父组件</button>
</div>
</template>
<script setup lang="ts" name="ChildComponent">
import { ref } from 'vue';
const birthdayGift = ref('一份全A成绩单');
defineProps(['car', 'giveBirthdayGift']);
</script>
自定义事件
在 Vue 3 中,子组件向父组件传值可以通过触发自定义事件来实现。
- 在子组件中,可以使用
defineEmits
宏来定义可以触发的自定义事件。然后在需要的地方,使用emit
函数触发事件并传递数据给父组件。
在子组件的<script setup>
部分,使用defineEmits
定义可以触发的事件:
html
<template>
<button @click="sendDataToParent">Send Data</button>
</template>
<script setup lang="ts" name="ChildComponent">
import { defineEmits } from 'vue';
const emit = defineEmits(['childEvent']);
function sendDataToParent() {
emit('childEvent', 'Data from child');
}
</script>
- 在父组件中,使用子组件标签时,可以通过
v-on
指令监听子组件触发的事件,并在事件处理函数中接收子组件传递过来的数据。
html
<template>
<div>
<ChildComponent @childEvent="handleChildEvent" />
</div>
</template>
<script setup>
import ChildComponent from './ChildComponent.vue';
function handleChildEvent(data) {
console.log('打印子组件传递的数据:', data);
}
</script>
使用 defineEmits()
自定义事件时,注意区分原生事件与自定义事件:
- 原生事件
- 由浏览器或 Vue 内置的 DOM 元素触发的事件,事件名称是特定的。例如
click
、mosueenter
、input
等。 - 事件对象
$event
: 是包含事件相关信息的对象(pageX
、pageY
、target
、keyCode
) - 可以直接在模板中使用
v-on
指令监听这些事件。 - 原生事件可以通过事件冒泡和捕获机制在 DOM 树中传播。
- 由浏览器或 Vue 内置的 DOM 元素触发的事件,事件名称是特定的。例如
- 自定义事件
- 由开发者在组件中定义并触发的事件,用于组件之间的通信。
- 事件名是任意名称。要为自定义事件选择有意义且与原生事件不同的名称,避免命名冲突。
- 事件对象
$event
: 是调用emit
时所提供的数据,可以是任意类型。 - 自定义事件通常只在组件之间进行通信,不会自动冒泡或捕获。
- 如果需要在组件层次结构中传播自定义事件,可以手动实现事件的传递和处理逻辑。
示例
在子组件中:
html
<script setup lang="ts" name="ChildComponent">
import { defineEmits } from 'vue';
const emit = defineEmits(['customEvent']);
function sendDataToParent() {
const dataToSend = 'Some data from child';
emit('customEvent', dataToSend);
}
</script>
使用 defineEmits()
定义了一个名为 customEvent
的自定义事件,然后在 sendDataToParent
函数中触发这个事件并传递数据。
在父组件中:
html
<template>
<div>
<ChildComponent @customEvent="handleChildEvent" />
</div>
</template>
<script setup lang="ts" name="Father">
function handleChildEvent(dataFromChild) {
console.log('Data from child:', dataFromChild);
}
</script>
父组件通过 v-on
指令监听 customEvent
自定义事件,并在 handleChildEvent
函数中接收子组件传递的数据。
v-model
v-model
是一个用于在表单元素或组件上实现双向数据绑定的指令。它允许在父组件和子组件之间自动同步数据,使得数据的变化可以在两个方向上进行传递。
v-model
的本质
v-model
的本质是语法糖,它是一种方便的方式来实现父子组件之间的双向数据绑定。
- 对于原生表单元素(如
<input>
、<textarea>
、<select>
等),v-model
实际上是结合了value
属性(对于<input>
和<textarea>
)或selectedValue
属性(对于<select>
)以及相应的input
、change
等事件。
html
<!-- 使用v-model指令 -->
<input type="text" v-model="message">
<!-- v-model的本质是下面这行代码 -->
<input
type="text"
:value="message"
@input="message=(<HTMLInputElement>$event.target).value"
/>
<!-- 对于一个<select>元素,v-model="selectedOption"相当于 -->
<select :value="selectedOption" @change="selectedOption = $event.target.value">
<option value="option1">Option 1</option>
<option value="option2">Option 2</option>
</select>
- 在自定义组件上:
v-model
的本质
当在自定义组件上使用v-model
时,需要在子组件中明确接收一个名为modelValue
的prop
,并在数据变化时触发一个名为update:modelValue
的自定义事件。
html
<!-- 父组件 -->
<template>
<div>
<ChildComponent v-model="parentData" />
<p>{{ parentData }}</p>
</div>
</template>
<script setup>
import ChildComponent from './ChildComponent.vue';
import { ref } from 'vue';
const parentData = ref('Initial value');
</script>
<!-- 子组件 -->
<template>
<div>
<!--将接收的value值赋给input元素的value属性,目的是:为了呈现数据 -->
<!--给input元素绑定原生input事件,触发input事件时,进而触发update:modelValue事件-->
<input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)">
</div>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
const props = defineProps({
modelValue: String,
});
const emit = defineEmits(['update:modelValue']);
</script>
父组件通过v-model
将parentData
绑定到子组件,子组件接收modelValue
prop 并在输入框的值变化时触发update:modelValue
事件,通知父组件更新数据。
多个v-model
绑定
v-model
默认是把数据绑定到属性value
上。可以更换value
,例如改成first
、second
等。
html
<!-- 父组件 -->
<template>
<div>
<ChildComponent v-model:first="firstData" v-model:second="secondData" />
</div>
</template>
<script setup>
import ChildComponent from './ChildComponent.vue';
import { ref } from 'vue';
const firstData = ref('First initial value');
const secondData = ref(42);
</script>
修饰符
-
.lazy
修饰符:- 默认情况下,
v-model
在每次输入事件后都会同步更新数据。使用.lazy
修饰符后,v-model
只会在blur
事件(表单元素失去焦点时)或按下回车键时更新数据。
- 默认情况下,
-
.number
修饰符:- 如果输入的值是一个字符串,但希望将其转换为数字类型,可以使用
.number
修饰符。
- 如果输入的值是一个字符串,但希望将其转换为数字类型,可以使用
-
.trim
修饰符:- 自动去除用户输入值两端的空格。
html
<template>
<div>
<input v-model.lazy="message">
<input v-model.number="numberValue">
<input v-model.trim="message">
</div>
</template>
$attrs
$attrs
是一个包含了父组件传递给当前组件的非 props 属性的对象。
作用和特点
- 传递未被处理的属性 :当父组件向子组件传递一些属性,但子组件没有通过 props 声明接收这些属性时,这些属性会被收集到
$attrs
对象中。这样可以方便地将这些属性继续传递给子组件内部的其他组件或元素。 - 避免属性重复声明 :如果子组件不需要对某些属性进行特殊处理,使用
$attrs
可以避免在子组件中重复声明这些属性,减少代码冗余。 - 支持多级传递 :
$attrs
可以在组件层级中逐级传递,使得属性可以方便地穿过多个中间组件,到达最终需要的组件。
因此,$attrs
可以用于实现当前组件的父组件 ,向当前组件的子组件 通信(祖→孙)。
示例
html
<!-- Father.vue -->
<template>
<div>
<ChildComponent someAttribute="value" />
</div>
</template>
<script setup name="Father">
import ChildComponent from './ChildComponent.vue';
</script>
<!-- ChildComponent.vue -->
<template>
<div>
<GrandchildComponent v-bind="$attrs" />
</div>
</template>
<script setup>
import GrandchildComponent from './GrandchildComponent.vue';
</script>
<!-- GrandchildComponent.vue -->
<template>
<!-- 可以直接通过$attrs访问属性 -->
<div>{{ $attrs.someAttribute }}</div>
</template>
<script setup>
// 也可以通过defineProps接收属性,接收后,不能再通过$attrs访问属性
defineProps(['someAttribute'])
</script>
父组件向子组件传递了 someAttribute
属性,子组件没有通过 props
接收这个属性,而是将 $attrs
传递给了孙组件,孙组件可以通过 $attrs
访问到这个属性。
$refs
、$parent
-
$refs
用于 :父→子。 -
$parent
用于:子→父。属性 说明 $refs
值为对象,包含所有被 ref
属性标识的DOM
元素或组件实例。$parent
值为对象,当前组件的父组件实例对象。
示例
html
<!-- Father.vue -->
<template>
<div>父组件的值:{{ parentValue }}</div>
<button @click="changeChildValue">修改子组件的值</button>
</template>
<script setup lang="ts" name="Father">
import { ref } from 'vue';
import Child from "./ChildComponent.vue";
const parentValue = ref(123)
const childRef = ref();
const changeChildValue = () => {
childRef.value.childValue = '白白的云朵';
}
// 必须要把父组件的属性暴露出去,子组件才能访问到
defineExpose({parentValue})
</script>
<!-- ChildComponent.vue -->
<template>
<div>子组件的值:{{ childValue }}</div>
<button @click="changeParentData($parent)">修改父组件的值</button>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import GrandChild from './GrandChild.vue';
const childValue = ref('蓝蓝的天空')
const changeParentData = (parent: {[key: string]: any} | null) => {
console.log(parent)
if(parent != null) {
console.log(parent.parentValue)
parent.parentValue++
}
}
// 必须要把父组件的属性暴露出去,父组件才能访问到
defineExpose({childValue})
</script>
provide
、inject
provide和inject
是一对用于实现依赖注入的组合式 API。它们允许祖先组件向其所有后代组件提供数据和方法,而无需通过层层传递props
。
provide
和inject
的作用范围是从提供数据或方法的组件开始,向下传递到所有后代组件。如果在中间的组件中再次提供相同键的数据或方法,会覆盖祖先组件提供的值。
注意,对注入的响应式数据的修改会影响所有注入了该数据的组件。
provide
:在祖先组件中,使用provide
函数来提供数据或方法。- 第一个参数是一个键(通常是一个字符串),用于标识提供的数据或方法。
- 第二个参数是要提供的值,可以是任何数据类型(如字符串、对象、函数等)或响应式对象
html
<script setup>
import { provide, reactive, ref } from 'vue';
// 提供一个字符串数据
provide('dataKey', 'Some data');
// 可以使用reactive或ref来创建响应式对象
const reactiveData = reactive({ message: 'Hello from ancestor' });
provide('reactiveDataKey', reactiveData);
// 提供一个方法
function sharedMethod() {
console.log('Shared method called.');
}
provide('methodKey', sharedMethod);
let injectedMoney = ref(100)
const injectedUpdateMoney = (value:number) => {
money.value += value
}
// 提供一个名为 moneyContext 的依赖,其值是一个包含 money 和 updateMoney 的对象。
provide('moneyContext',{ injectedMoney, injectedUpdateMoney })
</script>
inject
:在后代组件中,使用inject
函数来注入祖先组件提供的数据或方法。- 第一个参数是与provide中相同的键,用于指定要注入的数据或方法。
- 第二个参数是可选的默认值,如果祖先组件没有提供对应的值,则使用这个默认值。
html
<template>
<div>{{ injectedData }} - {{ injectedReactiveData.message }} -
<button @click="callInjectedMethod">Call Method</button>
</div>
</template>
<script setup lang="ts">
import { inject } from 'vue';
// 注入字符串数据,提供默认值
const injectedData = inject('dataKey', 'Default data');
// 可以使用类型标记进行类型检查
const injectedData1 = inject<string>('dataKey', 'Default value');
// 注入响应式对象
const injectedReactiveData = inject('reactiveDataKey');
// 注入方法
const injectedMethod = inject('methodKey');
function callInjectedMethod() {
injectedMethod();
}
// 直接接收对象
const injectedMoneyContext = inject('moneyContext');
// 解构赋值
let { injectedMoney, injectedUpdateMoney } = inject('moneyContext',{ injectedMoney: 0, injectedUpdateMoney: (x:number)=>{} })
</script>
使用inject
函数从祖先组件注入名为moneyContext
的依赖。如果祖先组件没有提供这个依赖,将使用默认值{ injectedMoney: 0, injectedUpdateMoney: (x: number) => {} }
,即一个包含初始值为 0
的injectedMoney
和一个空的更新函数updateMoney
。
使用解构赋值语法可以方便地从注入的对象中提取特定的属性 。在这里,将注入的对象中的injectedMoney
和injectedUpdateMoney
属性提取出来,分别赋值给当前组件中的同名变量,使得在当前组件的代码中可以直接使用这些变量来访问和操作注入的值和方法。
Pinia
mitt
mitt
是一个小型的事件发射器库,类似于EventEmitter
。
在 Vue 中可以使用mitt
来实现组件之间的全局事件通信,或者在一些特定场景下替代 Vue 的内置事件系统。
安装mitt
shell
npm install mitt
# 或者 yarn
yarn add mitt
订阅事件
-
on(eventName, handler)
:使用on
方法监听特定事件,并提供一个回调函数来处理事件。eventName
:要订阅的事件名称,类型为字符串或符号。handler
:事件触发时要执行的回调函数,该函数接收事件触发时传递的参数。
-
once(eventName, handler)
- 与
on
类似,但该回调函数只会在事件第一次触发时执行一次。
- 与
示例:
typescript
emitter.on('customClick', (data) => {
console.log('customClicked! Data:', data);
});
emitter.once('customLoad', (data) => {
console.log('customLoad once! Data:', data);
});
触发事件
emit(eventName,...args)
eventName
:要触发的事件名称,类型为字符串或符号。...args
:可选的参数,将传递给订阅该事件的回调函数。
使用emit
方法触发事件,并传递任意数量的参数:
typescript
emitter.emit('eventName', arg1, arg2,...);
取消订阅
off(eventName, handler)
eventName
:要取消订阅的事件名称,类型为字符串或符号。如果不提供此参数,则取消所有事件的订阅。handler
:要取消的特定回调函数。如果不提供此参数,则取消指定事件名称的所有回调函数。
示例:
typescript
const clickHandler = (data) => {
console.log('customClicked! Data:', data);
};
emitter.on('customClick', clickHandler);
// 稍后取消订阅
emitter.off('customClick', clickHandler);
清除所有订阅
all.clear()
:清除事件发射器实例上的所有订阅。
示例:
typescript
emitter.all.clear();
示例
在项目中导入mitt
并创建一个事件发射器实例:
typescript
// @/utils/emitter.ts
import mitt from 'mitt';
const emitter = mitt();
// 创建并暴露mitt
export default emitter
提供数据的组件,在合适的时候触发事件:
typescript
import emitter from "@/utils/emitter";
import { ref } from 'vue';
const birthdayGift = ref('一份全A成绩单');
function giveBirthdayGift(){
// 触发事件
emitter.emit('giveBirthdayGift', birthdayGift.value)
}
在接收数据的组件中,绑定事件、同时在销毁前解绑事件:
typescript
import emitter from "@/utils/emitter";
import { onUnmounted } from "vue";
// 绑定事件
emitter.on('getBirthdayGift',(value:string)=>{
console.log('getBirthdayGift事件被触发', value)
})
onUnmounted(()=>{
// 解绑事件
emitter.off('getBirthdayGift')
})
可以在 Vue 项目中将mitt
的事件发射器实例作为全局变量,在不同的组件中进行事件的触发和监听,实现全局事件通信。
typescript
// 在 main.ts 或其他全局入口文件中
import mitt from 'mitt';
const emitter = mitt();
app.config.globalProperties.$emitter = emitter;
在使用mitt
时,要考虑与 Vue 的生命周期的结合,确保事件的触发和监听在正确的时机进行。
slot
slot(插槽)允许父组件向子组件传递内容,使得子组件具有更高的可扩展性和灵活性。
默认插槽
如果子组件中只有一个未命名的<slot>
元素,它就是默认插槽:
html
<template>
<div>
<h3>子组件</h3>
<slot></slot>
</div>
</template>
父组件中可以直接在子组件标签内部传递内容,这些内容将被插入到子组件的默认插槽中:
html
<template>
<div>
<ChildComponent>
<p>这是传递给子组件默认插槽的内容</p>
</ChildComponent>
</div>
</template>
具名插槽
子组件中可以使用<slot>
元素来定义插槽,并可以为插槽指定一个名称:
html
<template>
<div>
<h3>子组件</h3>
<slot name="content"></slot>
</div>
</template>
父组件中使用带slot
属性的元素来向子组件的特定插槽传递内容:
html
<template>
<div>
<ChildComponent>
<template #content>
<p>这是传递给子组件插槽的内容</p>
</template>
</ChildComponent>
</div>
</template>
作用域插槽
基本概念:
- 传统插槽(非作用域插槽):父组件向子组件传递静态内容,子组件在特定位置渲染这些内容。父组件无法直接访问子组件内部的数据来动态决定插槽内容的渲染方式。
- 作用域插槽:子组件可以将数据暴露给父组件,父组件在使用插槽时可以通过解构赋值等方式获取这些数据,并根据数据动态地渲染插槽内容。
子组件定义作用域插槽 ,在子组件的模板中,使用<slot>
元素并通过v-bind
绑定数据:
html
<template>
<div>
<h3>子组件</h3>
<slot :data="slotData"></slot>
</div>
</template>
<script setup>
import { ref } from 'vue';
const slotData = ref('这是子组件传递给插槽的数据');
</script>
父组件使用作用域插槽 ,在父组件的模板中,使用带template
的元素来包裹插槽内容,并通过解构赋值获取子组件传递的数据:
html
<template>
<div>
<ChildComponent>
<!-- <template v-slot:default="{ data }"> 等同于 #default={data}-->
<template #default="{ data }">
<p>从子组件获取的数据:{{ data }}</p>
</template>
</ChildComponent>
</div>
</template>
在这个例子中,父组件通过解构赋值获取了子组件传递的data
,并在插槽内容中使用这个数据进行渲染。
作用域插槽实现了从子组件向父组件的数据传递。
动态插槽
通常情况下,插槽在组件中是固定的,一旦定义好了插槽的位置和名称,父组件就会按照这些固定的插槽来传递内容。但是,动态插槽允许在运行时根据不同的条件来切换使用不同的插槽,从而实现更加灵活的组件渲染。
在父组件中:
根据条件使用不同的插槽模板。可以使用v-if
、v-else
等指令来根据条件选择不同的插槽模板。
html
<template>
<div>
<ChildComponent>
<template v-if="showContentSlot">
<template #content>
<p>这是内容插槽的内容</p>
</template>
</template>
<template v-else>
<template #default>
<p>这是默认插槽的内容</p>
</template>
</template>
</ChildComponent>
</div>
</template>
<script setup>
import { ref } from 'vue';
const showContentSlot = ref(true);
</script>
在子组件中:
定义相应的插槽,以便父组件可以根据条件传递不同的内容。
html
<template>
<div>
<h3>子组件</h3>
<slot name="content"></slot>
<slot></slot>
</div>
</template>
子组件中定义了一个命名插槽content
和一个默认插槽,以接收父组件根据条件传递的内容。
动态插槽使得组件的渲染更加灵活,可以根据不同的条件动态地选择不同的插槽内容,适应不同的场景需求。