使用事件总线共享组件之间的Props

发布于 大漠

特别声明,本文根据@KINGSLEY SILAS的《Using Event Bus to Share Props Between Vue Components》一文所整理。

默认情况下,Vue组件之间的通讯是通过Props来完成的。Props是从父组件向子组件传递属性。例如,这里有一个组件,title是一个prop:

<blog-post title="My journey with Vue"></blog-post>

Props总是从父组件向子组件传递。随着应用程序复杂度的增加,你会慢慢遇到所谓的Prop Drilling,这里有一篇文章介绍了这方面的东西,虽然是React方面的,但也适用于Vue。Prop Drilling是将props向下、向下、向下传递给子组件的想法 —— 正如你想象的那样,这通常是一个乏味的过程。

因此,繁琐的Prop Drilling可能是一个复杂的潜在问题。另一个与不相关的组件之间的通讯有关。我们可以通过使用事件总线来解决这些问题。

什么是事件总线?这个名字本身就是一个总结。这是一个组件将props从一个组件传递到另一个组件的一种运输方式,无论这些组件伴于树的哪个位置。

练习任务:建立一个计数器

让我们一起构建一些东西,用来帮助我们演示事件总线相关的概念。一个计数器,增加或减少input中输入的数值,并记录总数。

为了使用事件总线,我们首先需要对事件总线进行初始化。可以在src下创建一个event-bus.js文件,添加下面这段代码,对事件总线进行初始化:

// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()

除了像上面那样对事件进行初始化之外,还可以对事件总线进行全局初始化。有关于这方面的详细介绍,可以阅读前段时间整理的一篇文章《Vue 2.0学习笔记:事件总线(EventBus)》。

这里将Vue实例设置为EventBus。当然,你也可以随便给它起一个你喜欢的名字。如果你使用的是一个单文件组件,那么你应该在一个单独的文件中添加下面这段代码,因为无论如何你都必须导出(export)分配给EventBusVue实例。

import Vue from 'vue';
export const eventBus = new Vue();

这样我们就可以在计数器组件(Counter)中使用它了。不过在上面的示例中,为EventBus初始化单独创建了一个event-bus.js,在这个文件中添加了初始化EventBus的代码。在Counter组件可以像下面这样调用已初始化的EventBus

<!-- Counter.vue -->
import { EventBus } from "../event-bus.js";

接下一我们要做的是:

  • 我们想要一个初始值为0count
  • 我们需要一个接受数值的输入框input
  • 我们需要两个按钮button:一个在单击时将提交的数值添加到计数(count)中,另一个将在单击时从计数中减去提交的数值
  • 我们想确认当计数改变时发生了什么

我们在Counter中的模板可以这样写:

<!-- Counter.vue -->
<template>
    <div class="counter">
        <h3>{{ count }}</h3>
        <div :class="$style.form">
            <button @click.prevent="handleIncrement" :class="[$style.btn, $style['btn-increment']]">+</button>
            <input type="number" v-model="entry" :class="$style.input" />
            <button @click.prevent="handleDecrement" :class="[$style.btn, $style['btn-decrement']]">-</button>
        </div>
        <div class="form-help">{{ text }}</div>
    </div>
</template>

通过v-modelentry的值绑定到input,根据用户输入的内容,我们将使用该值来增加或减少计数。当单击任何一个按钮时,都会触发一个方法,该方法会增加或减少count的值。最后,在<p>标签中包含的{{ text }}会打印出提示消息(当count值更改后)。

以下是Counter组件对应的脚本代码:

<!-- Counter.vue -->
<script>
    import { EventBus } from "../event-bus.js";
    export default {
        name: "Counter",
        data() {
            return {
                count: 0,
                entry: 3,
                text: ''
            };
        },
        created () {
            EventBus.$on('count-incremented', () => {
                this.text = `Count was increased`
                setTimeout(() => {
                    this.text = ''
                }, 3000)
            })

            EventBus.$on('count-decremented', () => {
                this.text = `Count was decreased`
                setTimeout(() => {
                    this.text = ''
                }, 3000)
            })
        },
        methods: {
            handleIncrement () {
                this.count += parseInt(this.entry, 10)
                EventBus.$emit('count-incremented')
                this.entry = 0
            },
            handleDecrement () {
                this.count -= parseInt(this.entry, 10)
                EventBus.$emit('count-decremented')
                this.entry = 0
            }
        }
    };
</script>

我们要做的第一件事是建立一个从一个组件发送事件到另一个组件的路径。我们可以使用EventBus.$emit()(通过$emit())来铺路。这种发送包含在两个方法中,handleIncrementhandleDecrement,这两个方法用来监听input的提交。一旦事件发生,我们的事件总线就会向任何请求数据的绷件发送Props

你可能已经注意到,我们正在使用EventBus.$on()created()钩子中侦听这两个事件。在这两个事件中,我们必须传递与我们发出的事件相对应的字符串。这类似于特定事件的标识符,以及为组件建立接收数据方式的标识符。当EventBus识别已宣布的特定事件时,接下来的函数将被调用 —— 我们设置一个文本来显示发生了什么,并在三秒钟后使其消失。

练习任务:处理多个组件

假设我们正在开发一个配置文件页面,用户可以在该页面上更新自己的名字和电子邮件地址,然后在不刷新页面的情况下查看更新。即使我们要处理两个组件,也可以使用事件总线来实现这个效果。

在上面的这个示例中,我们创建了ShowProfileEditProfile两个组件,前者用来显示个人信息,后者用来编辑个人信息。两个组件的<template>分别如下:

<!-- ShowProfile.vue -->
<template>
    <div :class="$style.profile">
        <h1>{{ name }}</h1>
        <p>{{ email }}</p>
    </div>
</template>

<!-- EditProfile.vue -->
<template>
    <div :class="$style['edit-profile']">
        <h3>Enter your details below</h3>
        <form @submit.prevent="handleSubmit">
            <div :class="$style['form-controle']">
                <label for="name">Name</label>
                <input type="text" v-model="user.name" id="name" />
            </div>
            <div :class="$style['form-controle']">
                <label for="email">Email</label>
                <input type="email" v-model="user.email" id="email" />
            </div>
            <div :class="$style['form-controle']">
                <button>Submit</button>
            </div>
        </form>
    </div>
</template>

我们把idsuser.nameuser.email)传递相应的组件。首先,让我们为EditProfile组件设置模板,它包含我们想要传递给ShowProfile组件的nameemail数据。同样,我们也和前面的示例一样,创建了一个事件总线,在它监听到submit事件后发出该数据。

<!-- EditProfile.vue -->
<script>
    import { EventBus } from "../event-bus.js";
    export default {
        name: "EditProfile",
        data() {
            return {
                user: {
                    name: "",
                    email: ""
                }
            };
        },
        methods: {
            handleSubmit() {
                EventBus.$emit("form-submitted", this.user);
                this.user = {};
            }
        }
    };
</script>

ShowProfile组件中的data主要用于用户提交数据时,ShowProfile中的data进行反应性(Reactively)更新,该组件查找事件总线到达其集线器时输入的nameemail

<!-- ShowProfile.vue -->
<script>
    import { EventBus } from "../event-bus.js";
    export default {
        name: "ShowProfile",
        data() {
            return {
                name: "W3cplus",
                email: "w3cplus@hotmail.com"
            };
        },
        created() {
            EventBus.$on("form-submitted", ({ name, email }) => {
                this.name = name;
                this.email = email;
            });
        }
    };
</script>

很酷,对吧!尽管ShowProfileEditProfile组件是兄弟关系(不是直接的父子关系),但是它们可以通过相同的事件进行数据通讯。

总结

事件总线想在应用程序中激活反应的情况下是很有帮助的。具体来说,就是从服务器获得向应更新组件,而不会导致页面刷新。还可能有多个组件可以侦听发出的事件。如果你有其他关于使用事件总线的有趣场景,欢迎在下面的评论中与我们共享。

扩展阅读

  • 事件总线(EventBus)
  • Vue组件通讯
  • Vue中的状态管理
  • 不同场景下组件间的数据通讯
  • 实现组件数据的双向绑定
  • 组件数据传递Zoom With Nike