Bootstrap

Vue组件通信,如何搭建数据桥梁

前言

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 函数到按钮上。

创建全局事件总线

  1. 创建事件总线:
  • 创建一个 Vue 实例作为事件总线。

2.暴露事件总线:

    • 为了让其他组件能够使用这个事件总线,我们需要将其暴露出去。

示例代码

Javascript
深色版本
// event-bus.js
import { createApp } from 'vue';

// 创建一个空的 Vue 实例作为事件总线
const eventBus = createApp({});

// 导出事件总线
export default eventBus.config.globalProperties.$bus;

使用全局事件总线

一旦创建了全局事件总线,你可以在任何组件中使用它来触发事件和监听事件。

发布事件

  1. 发布事件:
  • 在组件中使用 eventBus.$emit 方法触发事件,并传递数据。

订阅事件

  1. 订阅事件:
  • 在组件中使用 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>
;