前言
Vue组件化的思想,让前端的代码编写起来更加的优雅,让各个组件变成独立的、可复用的模块,这无疑提高了开发应用的效率降低了开发人员的压力。但是这样就诞生一个问题: 当组件变成独立的模块时,各个组件之间如果要进行数据的交互,应该如何进行操作呢。 这里就要利用到这个知识: ‘组件通信’,而在组件之间的通信中,也分为几个不同的通信场景,在不同的场景下,将要采取不同的通信方法。有以下几种通信场景:
①父子通信(父组件与子组件之间的通信)
②兄弟通信(父组件的两个子组件之间的通信)
③跨级组件通信(比如祖孙组件等跨级别的通信)
④全局事件总线
父子通信
父子通信包括两种情况,第一种是父组件向子组件发送数据,第二种是子组件向父组件发送数据。
这里我们用一个实战例子,能够更形象展示出Vue组件通信的使用场景。
场景的主要内容很简单,就是一个输入框input标签和ul标签,初始化一个列表数组['html','css'],通过用input标签中输出的值,点击确定后将该值push进列表数组,通过ul标签将数组内容展示出来。效果如下:
1.父向子通信
此时在该例子中,我们把输入框和列表数据作为父组件,ul列表的渲染作为子组件,此时当父组件进行完点击确定事件时,列表数组进行更新,我们需要通过父组件向子组件传递这个列表数组,子组件通过defineProps接收数据,从而进行渲染ul,具体代码如下:
parent组件
<template>
<div class="header">
<input type="text" v-model="newMsg">
<button @click="add">确定</button>
</div>
<child :data="list"></child>
</template>
<script setup>
import {ref} from 'vue'
import child from './child.vue'
const newMsg = ref('')
const list = ref(['html','css'])
const add = ()=>{
list.value.push(newMsg.value)
newMsg.value = '' // 重置输入框
}
</script>
child组件
<template>
<div class="body">
<ul>
<li v-for="item in data">{{ item }}</li>
</ul>
</div>
</template>
<script setup>
defineProps({
data:{
type: Array,
default:()=>[]
}
})
</script>
2.子向父通信
实际上在一些子组件需要修改父组件的数据时,子组件可以不向父组件中传递数据,直接对父组件传来的数据进行修改,但是Vue框架中的数据流思想是单向的流向,不建议子组件直接修改父组件的数据,这样会导致数据流向变得混乱。所以在这种情况下以及其他某些情况下我们需要用到子向父通信,在子向父通信有很多种,这里我们列举出了这三种方式:
①发布订阅通过子组件给父组件传递参数,父组件进行修改,
②v-model子组件通过接收props直接操作数据,并通过事件告知父组件
③通过defineExpose()暴露子组件的属性,让父组件以ref获取子组件dom的方式得到该属性
①通过发布订阅的方式(实质是在父组件中修改数据)
子组件通过调用defineEmits('事件名')方法,创建一个事件对象,并且在接收修改后的数据时发布这个事件并携带想要传递的数据,父组件通过订阅这个事件绑定一个函数,当订阅的事件被子组件触发时,此函数会携带着传递的数据作为函数的参数。在我们的使用场景中,父组件为ul列表并携带列表数组,子组件为输入框input。此时通过子组件创建一个emit对象,在点击事件中发布该事件对象emit,并携带输入框中的数据。在父组件中订阅该事件,并绑定函数handle,在handle中就能接收到参数--也就是输入框中的数据。
parent组件
<template>
<child @addMsg="handle"></child>
<div class="body">
<ul>
<li v-for="item in list">{{ item }}</li>
</ul>
</div>
</template>
<script setup>
import {ref} from 'vue'
import child from './child.vue'
const list = ref(['html','css'])
const handle = (e)=>{
list.value.push(e)
}
</script>
child组件
<template>
<div class="header">
<input type="text" v-model="newMsg">
<button @click="add">确定</button>
</div>
</template>
<script setup>
import {ref} from 'vue'
const newMsg = ref('')
const emit = defineEmits(['addMsg']) // 定义事件
const add = ()=>{
emit('addMsg',newMsg.value) // 发布
}
</script>
②通过v-model的方式通信(实质上是在子组件中修改数据)
实质上,v-model双向绑定数据进行通信是直接通过props和emits来实现。在这个例子中,v-model
实质上是通过 props
和自定义事件 (update:list
) 来实现双向绑定的。在props接收到数据list并进行修改后,通过自定义事件const emits = defineEmits('update:list')进行定义事件,(注意此处updata不可随意命名,若格式不一致,则需要在父组件的v-model中重新绑定)接收修改后的list作为emits的第二个参数进行发布(第一个参数为emits的名称'updata:list'),v-model此时就能自动接收到updata:list这个事件,并且双向绑定的数据list会自动响应式更新。
parent组件
<template>
<child v-model:list="list"></child>
<div class="body">
<ul>
<li v-for="item in list">{{ item }}</li>
</ul>
</div>
</template>
<script setup>
import {ref} from 'vue'
import child from './child.vue'
const list = ref(['html','css'])
</script>
child组件
<template>
<div class="header">
<input type="text" v-model="newMsg">
<button @click="add">确定</button>
</div>
</template>
<script setup>
import {ref} from 'vue'
const props = defineProps({
list:[]
})
const newMsg = ref('')
const emits = defineEmits(['updata:list'])
const add = ()=>{
// props.list.push(newMsg.value)
const arr = props.list
arr.push(newMsg.value)
emits('updata:list',arr)
}
</script>
③通过defineExpose()方式通信
主要通过父组件使用ref,绑定child组件的dom结构,在child组件中使用defineExpose()方法,将list暴露出来,让ref绑定的dom结构能拿到这个属性。
parent组件
<template>
<child ref="childRef"></child>
<div class="body">
<ul>
<li v-for="item in childRef?.list">{{ item }}</li>
</ul>
</div>
</template>
<script setup>
import {onMounted, ref} from 'vue'
import child from './child.vue'
const childRef = ref(null)
onMounted(()=>{
console.log(childRef.value.list);
})
</script>
child组件
<template>
<div class="header">
<input type="text" v-model="newMsg">
<button @click="add">确定</button>
</div>
</template>
<script setup>
import {ref} from 'vue'
const newMsg = ref('')
const list = ref(['html','css'])
const add = ()=>{
list.value.push(newMsg.value)
}
defineExpose({list})
</script>
兄弟通信
兄弟通信大体有两种简便的方法:
①通过父组件为中转站,进行数据通信
②通过状态管理仓库Vuex,pinia...等,进行全局的数据管理
1.以父组件为中转站的方式,也就是上述父子组件通信的double版本,进行两次的父子通信。
2.通过状态管理仓库,通俗的将就是将数据统一放到一个全局的仓库中,所有组件都能够取得到所需的数据,并且能够响应式地修改数据,使得数据动态地流动,这就是仓库的灵活之处,那仓库应该怎么用呢,这里我以pinia为例。
1. 安装 Pinia
首先,你需要安装 Pinia。可以通过 npm 或 yarn 进行安装:
Bash
深色版本
1# 使用 npm
2npm install pinia
3
4# 或者使用 yarn
5yarn add pinia
2. 创建 Pinia 实例
在你的主应用文件(通常是 main.js
或 main.ts
)中,创建一个 Pinia 实例,并将它挂载到 Vue 应用程序上
一个简易的仓库文件创建
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useQuestionStore = defineStore('question',()=>{
const score = ref(0)
function addScore(){
score++
}
return{
score,
addScore
}
})
仓库的调用 pinia仓库的调用也很简单,将仓库所在的文件引入,解构出useQuestionStore方法,利用它的返回值,就可以进行数据的利用和修改了。
跨级组件通信
跨级组件通信大体有三种方法:
①仍然是通过父组件为中转站,进行数据通信,但这种比较费时间和精力,不方便管理。
②仍然是通过状态管理仓库Vuex,pinia...等,进行全局的数据管理。
③通过provide,inject,提供数据并注入的方式。
下面主要讲解第三种方式:在vue3中,privide被新的exposeExpose()方法代替了,以下是使用步骤
1.提供数据:
- 在祖先组件中使用
provide
选项来提供数据或方法。 provide
接受一个对象或返回对象的函数,其中键是后代组件注入时所使用的键名,值是要注入的数据或方法。
2.注入数据:
- 在后代组件中使用
inject
选项来注入祖先组件提供的数据。 inject
也是一个接受键名的选项,可以是一个字符串或一个配置对象。
示例
下面是一个具体的例子,展示如何使用 provide
和 inject
在 Vue 3 中进行跨层级组件通信。
祖先组件 (Grandparent.vue)
Vue
深色版本
1<template>
2 <div>
3 <h2>Grandparent Component</h2>
4 <parent></parent>
5 </div>
6</template>
7
8<script setup>
9import Parent from './Parent.vue';
10
11// 提供数据
12const provideData = {
13 message: 'Hello from Grandparent!',
14 greet: () => console.log('Greet from Grandparent!'),
15};
16
17defineExpose(provideData); // Vue 3 中使用 defineExpose 曝露数据
18</script>
父组件 (Parent.vue)
Vue
深色版本
1<template>
2 <div>
3 <h2>Parent Component</h2>
4 <child></child>
5 </div>
6</template>
7
8<script setup>
9import Child from './Child.vue';
10
11// 父组件不需要做任何事情,只是作为一个中介
12</script>
子组件 (Child.vue)
Vue
深色版本
1<template>
2 <div>
3 <h2>Child Component</h2>
4 <p>{{ message }}</p>
5 <button @click="greet">Greet</button>
6 </div>
7</template>
8
9<script setup>
10// 注入数据
11const { message, greet } = inject('provideData');
12
13// 如果希望提供默认值
14// const { message, greet } = inject('provideData', { message: 'Default Message', greet: () => {} });
15</script>
说明
Grandparent.vue:
- 使用
defineExpose
曝露数据,这些数据将在后代组件中通过inject
获取。 - 在这里,我们提供了
message
和greet
函数。
Parent.vue:
- 父组件不需要做任何特殊的事情,它只是一个中间组件,数据会自动向下传递。
Child.vue:
- 使用
inject
来获取祖先组件提供的数据。 inject
接受一个键名和一个默认值(可选)。- 在这个例子中,我们获取了
message
和greet
函数,并在模板中显示message
,同时绑定greet
函数到按钮上。
创建全局事件总线
- 创建事件总线:
- 创建一个 Vue 实例作为事件总线。
2.暴露事件总线:
-
- 为了让其他组件能够使用这个事件总线,我们需要将其暴露出去。
示例代码
Javascript
深色版本
// event-bus.js
import { createApp } from 'vue';
// 创建一个空的 Vue 实例作为事件总线
const eventBus = createApp({});
// 导出事件总线
export default eventBus.config.globalProperties.$bus;
使用全局事件总线
一旦创建了全局事件总线,你可以在任何组件中使用它来触发事件和监听事件。
发布事件
- 发布事件:
- 在组件中使用
eventBus.$emit
方法触发事件,并传递数据。
订阅事件
- 订阅事件:
- 在组件中使用
eventBus.$on
方法监听事件,并定义处理逻辑。
- 取消订阅:
- 使用
eventBus.$off
方法取消订阅事件,以避免内存泄漏。
示例代码
假设我们有两个组件,ComponentA
和 ComponentB
,它们需要相互通信。
ComponentA.vue
Vue
深色版本
<template>
<button @click="sendData">Send Data</button>
</template>
<script setup>
import eventBus from './event-bus';
// 发送数据
const sendData = () => {
eventBus.$emit('data-event', 'Hello from ComponentA!');
};
</script>
ComponentB.vue
Vue
深色版本
<template>
<div>
<p>Message: {{ message }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue';
import eventBus from './event-bus';
// 接收数据
const message = ref('');
// 监听事件
const handleMessage = (msg) => {
message.value = msg;
};
// 使用 eventBus 订阅事件
eventBus.$on('data-event', handleMessage);
// 在组件销毁前取消订阅
onBeforeUnmount(() => {
eventBus.$off('data-event', handleMessage);
});
</script>