VueJs 2.0中同级组件之间的通信


112

总览

在Vue.js 2.x中,model.sync不推荐使用

那么,在Vue.js 2.x中的兄弟组件之间进行通信的正确方法是什么?


背景

据我了解Vue 2.x,同级通信的首选方法是使用商店或事件总线

根据Evan(Vue的创建者)的说法:

值得一提的是,“在组件之间传递数据”通常不是一个好主意,因为最终数据流变得不可跟踪且很难调试。

如果一条数据需要由多个组件共享,则首选 全局存储Vuex

[ 讨论链接 ]

和:

.once并且.sync已弃用。现在,道具总是单向下降。为了在父范围内产生副作用,组件需要显式地声明emit一个事件,而不是依赖于隐式绑定。

因此,Evan 建议使用$emit()$on()


顾虑

让我担心的是:

  • 每个storeevent具有全球知名度(纠正我,如果我错了);
  • 为每次次要沟通创建一个新的商店太浪费了;

我想要的是兄弟姐妹组件的某种范围 eventsstores可见性。(或者也许我不理解上面的想法。)


那么,同级组件之间进行通信的正确方法是什么?


2
$emit结合v-model模仿.sync。我认为您应该采用Vuex方式
eltonkamami

3
所以我也考虑过同样的问题。我的解决方案是将事件发射器与等效于“作用域”的广播通道一起使用-即,子/父级和同级设置使用同一通道进行通信。就我而言,我使用广播库radio.uxder.com,因为它只是几行代码和它的防弹功能,但是许多人会选择节点EventEmitter。
Tremendus Apps

Answers:


83

在Vue 2.0中,我使用的是eventHub机制,如文档所示

  1. 定义集中式事件中心。

    const eventHub = new Vue() // Single event hub
    
    // Distribute to components using global mixin
    Vue.mixin({
        data: function () {
            return {
                eventHub: eventHub
            }
        }
    })
  2. 现在在您的组件中,您可以发出事件

    this.eventHub.$emit('update', data)
  3. 听你的话

    this.eventHub.$on('update', data => {
    // do your thing
    })

更新 请查看@alex的答案,该答案描述了一个更简单的解决方案。


3
请注意:密切注意Global Mixins,并尽可能避免使用它们,因为根据此链接vuejs.org/v2/guide/mixins.html#Global-Mixin,它们甚至可能影响第三方组件。
Vini.g.fer

6
一个更简单的解决方案是使用什么@Alex描述- this.$root.$emit()this.$root.$on()
Webnet

5
为了将来参考,请不要用别人的答案来更新您的答案(即使您认为更好,也可以参考它)。链接到替代答案,或者甚至让OP如果您认为应该接受另一个答案,但是将他们的答案复制到您自己的答案中是不好的形式,并且会阻止用户给予应有的信誉,因为他们可能只会投票支持您的只回答。鼓励他们不要在自己的答案中包括该答案,以导航(并从而投票)您所参考的答案。
GrayedFox

4
感谢您提供宝贵的反馈@GrayedFox,并相应地更新了我的答案。
卡科尼

2
请注意,Vue 3将不再支持该解决方案。请参阅 stackoverflow.com/a/60895076/752916
AlexMA

145

您甚至可以使其更短,并将 Vue实例用作全局事件中心:

组件1:

this.$root.$emit('eventing', data);

组件2:

mounted() {
    this.$root.$on('eventing', data => {
        console.log(data);
    });
}

2
这比定义附加事件中心并将其附加到任何事件消费者更好。
schad

2
我非常喜欢这种解决方案,因为我真的不喜欢具有范围的事件。但是,我并不是每天都在使用VueJS,所以我很好奇是否有人在这种方法上遇到问题。
网络

2
所有答案的最简单解决方案
Vikash Gupta,

1
美观,简短且易于实施,也易于理解
娜达

1
如果只希望直接进行同级兄弟通信,请使用$ parent而不是$ root
Malkev '19

46

通讯类型

在设计Vue应用程序(或者实际上是任何基于组件的应用程序)时,有不同的通信类型,这取决于我们要处理的问题,并且它们具有自己的通信通道。

业务逻辑:指特定于您的应用及其目标的所有内容。

表示逻辑:用户与之交互或由用户交互产生的任何结果。

这两个问题与以下类型的通信有关:

  • 应用状态
  • 亲子
  • 儿童父母
  • 兄弟姐妹

每种类型都应使用正确的通信渠道。


沟通渠道

渠道是一个宽松的术语,我将用它来指代具体实现以围绕Vue应用程序交换数据。

道具:亲子陈述逻辑

Vue中用于直接父子沟通的最简单的沟通渠道。它应主要用于在层次结构中传递与表示逻辑有关的数据或一组受限制的数据。

参考和方法:演示反模式

如果没有必要使用道具让孩子处理来自父母的事件,则在子组件上设置一个对象ref并调用其方法就可以了。

不要那样做,这是一种反模式。重新考虑您的组件体系结构和数据流。如果您发现自己想从父级调用子级组件上的方法,则可能是时候提升状态或考虑此处所述或其他答案中所述的其他方式了。

活动:儿童-父母陈述逻辑

$emit$on。直接儿童-父母沟通的最简单的沟通渠道。同样,应将其用于表示逻辑。

活动巴士

大多数答案为事件总线提供了很好的选择,事件总线是可用于远程组件或实际上任何组件的通信通道之一。

当将道具从最远处传递到深层嵌套的子组件时,几乎不需要其他组件之间的传递时,这将变得很有用。谨慎使用谨慎选择的数据。

请注意:随后将自身绑定到事件总线的组件的创建将被绑定一次以上-导致触发多个处理程序和泄漏。我个人从来没有感到过去设计过的所有单页应用程序都需要事件总线。

以下内容演示了一个简单的错误如何导致泄漏,Item即使从DOM中删除该组件仍会触发该泄漏。

记住要删除destroyed生命周期挂钩中的侦听器。

集中存储(业务逻辑)

Vuex是与Vue一起进行状态管理的一种方式。它提供的不仅是事件,还可以进行全面的应用。

现在你问

[S]我应该为每次次要沟通创建vuex的商店吗?

当以下情况时,它确实发光:

  • 处理您的业务逻辑,
  • 与后端(或任何数据持久层,例如本地存储)进行通信

因此,您的组件可以真正专注于它们的本意,即管理用户界面。

这并不意味着您不能将其用于组件逻辑,但我会将其范围限定为仅具有必要的全局UI状态的命名空间Vuex模块。

为了避免在全局状态下处理所有混乱情况,应将存储分为多个命名空间模块。


组件类型

为了协调所有这些通信并简化可重用性,我们应该将组件视为两种不同的类型。

  • 应用程式专用容器
  • 通用组件

同样,这并不意味着应该重用通用组件或不能重用特定于应用程序的容器,但是它们具有不同的职责。

应用程式专用容器

这些只是包装其他Vue组件(通用或其他特定于应用程序的容器)的简单Vue组件。这就是Vuex商店通信的地方,此容器应通过其他更简单的方式(例如道具和事件监听器)进行通信。

这些容器甚至可能根本没有本机DOM元素,并让通用组件处理模板和用户交互。

范围莫名其妙eventsstores能见度兄弟姐妹组件

这是范围界定的地方。大多数组件都不知道商店,并且该组件((大多数情况下)应该使用一个命名空间的商店模块,具有有限的一组,gettersactions与提供的Vuex绑定帮助程序一起应用)

通用组件

这些应该从道具接收数据,对自己的本地数据进行更改,并发出简单的事件。大多数时候,他们根本不知道Vuex商店的存在。

它们也可以称为容器,因为它们的唯一职责是分派到其他UI组件。


兄弟交流

那么,毕竟,我们应该如何在两个同级组件之间进行通信?

通过一个示例更容易理解:说我们有一个输入框,它的数据应该在应用程序(树中不同位置的兄弟姐妹)之间共享,并使用后端持久化。

最坏的情况开始,我们的组件将混合呈现业务逻辑。

// MyInput.vue
<template>
    <div class="my-input">
        <label>Data</label>
        <input type="text"
            :value="value" 
            :input="onChange($event.target.value)">
    </div>
</template>
<script>
    import axios from 'axios';

    export default {
        data() {
            return {
                value: "",
            };
        },
        mounted() {
            this.$root.$on('sync', data => {
                this.value = data.myServerValue;
            });
        },
        methods: {
            onChange(value) {
                this.value = value;
                axios.post('http://example.com/api/update', {
                        myServerValue: value
                    })
                    .then((response) => {
                        this.$root.$emit('update', response.data);
                    });
            }
        }
    }
</script>

为了分离这两个问题,我们应该将组件包装在特定于应用程序的容器中,并将表示逻辑保留在通用输入组件中。

我们的输入组件现在是可重用的,并且不了解后端或同级组件。

// MyInput.vue
// the template is the same as above
<script>
    export default {
        props: {
            initial: {
                type: String,
                default: ""
            }
        },
        data() {
            return {
                value: this.initial,
            };
        },
        methods: {
            onChange(value) {
                this.value = value;
                this.$emit('change', value);
            }
        }
    }
</script>

现在,我们特定于应用程序的容器可以成为业务逻辑和演示通信之间的桥梁。

// MyAppCard.vue
<template>
    <div class="container">
        <card-body>
            <my-input :initial="serverValue" @change="updateState"></my-input>
            <my-input :initial="otherValue" @change="updateState"></my-input>

        </card-body>
        <card-footer>
            <my-button :disabled="!serverValue || !otherValue"
                       @click="saveState"></my-button>
        </card-footer>
    </div>
</template>
<script>
    import { mapGetters, mapActions } from 'vuex';
    import { NS, ACTIONS, GETTERS } from '@/store/modules/api';
    import { MyButton, MyInput } from './components';

    export default {
        components: {
            MyInput,
            MyButton,
        },
        computed: mapGetters(NS, [
            GETTERS.serverValue,
            GETTERS.otherValue,
        ]),
        methods: mapActions(NS, [
            ACTIONS.updateState,
            ACTIONS.updateState,
        ])
    }
</script>

由于Vuex存储操作处理的是后端通信,因此我们这里的容器不需要了解axios和后端。


3
有关方法是“评论表示赞同相同的耦合,使用道具
ghybs

我喜欢这个答案。但是,您能否详细说明Event Bus和“注意:”注释?也许您可以举个例子,我不理解组件如何被绑定两次。
vandroid

您如何在父组件和子组件之间进行沟通,例如表单验证。父组件是页面,子组件是表单,子组件是输入表单元素?
泽德勋爵

1
@vandroid我创建了一个简单的示例,当未正确删除侦听器时显示泄漏,如该线程中的每个示例。
Emile Bergeron '18

@LordZed这确实取决于,但据我对您的情况的了解,这看起来像是一个设计问题。Vue应该主要用于表示逻辑。表单验证应在其他地方完成,例如在原始JS API接口中进行,Vuex操作将使用表单中的数据进行调用。
Emile Bergeron '18

10

好的,我们可以使用v-on事件通过父级在兄弟姐妹之间进行通信。

Parent
 |-List of items //sibling 1 - "List"
 |-Details of selected item //sibling 2 - "Details"

假设Details单击中的某些元素时需要更新组件List


Parent

模板:

<list v-model="listModel"
      v-on:select-item="setSelectedItem" 
></list> 
<details v-model="selectedModel"></details>

这里:

  • v-on:select-item这是一个事件,将在List组件中调用(请参见下文);
  • setSelectedItem这是Parent更新的方法selectedModel;

JS:

//...
data () {
  return {
    listModel: ['a', 'b']
    selectedModel: null
  }
},
methods: {
  setSelectedItem (item) {
    this.selectedModel = item //here we change the Detail's model
  },
}
//...

List

模板:

<ul>
  <li v-for="i in list" 
      :value="i"
      @click="select(i, $event)">
        <span v-text="i"></span>
  </li>
</ul>

JS:

//...
data () {
  return {
    selected: null
  }
},
props: {
  list: {
    type: Array,
    required: true
  }
},
methods: {
  select (item) {
    this.selected = item
    this.$emit('select-item', item) // here we call the event we waiting for in "Parent"
  },
}
//...

这里:

  • this.$emit('select-item', item)select-item直接通过父项发送项目。父级会将其发送到Details视图

5

如果要“破解” Vue中的常规通信模式(特别是现在.sync不建议使用),通常要做的是创建一个处理组件之间通信的简单EventEmitter。从我的最新项目之一:

import {EventEmitter} from 'events'

var Transmitter = Object.assign({}, EventEmitter.prototype, { /* ... */ })

Transmitter然后,您可以使用此对象在任何组件中进行操作:

import Transmitter from './Transmitter'

var ComponentOne = Vue.extend({
  methods: {
    transmit: Transmitter.emit('update')
  }
})

并创建一个“接收”组件:

import Transmitter from './Transmitter'

var ComponentTwo = Vue.extend({
  ready: function () {
    Transmitter.on('update', this.doThingOnUpdate)
  }
})

同样,这是针对特定用途的。不要将整个应用程序都基于这种模式,而应使用类似的方法Vuex


1
我已经在使用vuex,但是我还是应该为每个次要通信创建vuex的存储吗?
谢尔盖·潘菲洛夫

我很难说出这么多的信息,但是我想说的是,如果您已经在使用vuexyes,那就去吧。用它。
赫克托·洛伦佐

1
实际上,我不同意我们在每次小的交流时都需要使用vuex ...
Victor

不,当然不是,这完全取决于上下文。实际上,我的答案与vuex无关。另一方面,我发现使用vuex和中央状态对象的概念越多,我对对象之间的通信的依赖就越少。但是,是的,同意,这完全取决于。
赫克托·洛伦佐

3

如何处理兄弟姐妹之间的通信取决于情况。但是首先我要强调的是,Vue 3中不再使用全局事件总线方法。请参阅此RFC。因此,为什么我决定写一个新答案。

最低共同祖先模式(或“ LCA”)

对于简单的情况,我强烈建议使用“最低公共祖先”模式(也称为“数据关闭,事件增加”)。这种模式易于阅读,实施,测试和调试。

从本质上讲,这意味着如果两个组件需要通信,请将它们的共享状态放到作为祖先共享的最接近的组件中。通过prop将数据从父组件传递到子组件,并通过发出事件将信息从子组件传递到父(请参见此答案底部的示例)。

举一个人为的例子,在电子邮件应用程序中,如果“收件人”组件需要与“邮件正文”组件进行交互,则该交互的状态可以存在于其父级(可能是称为的组件email-form)中。您可能在email-form被叫中有一个道具,addressee以便邮件正文可以自动添加Dear {{addressee.name}}到基于接收器的电子邮件地址的电子邮件。

如果通信必须与许多中间人一起进行很长距离的通信,LCA将变得很繁重。我经常推荐同事阅读这篇出色的博客文章。(忽略其示例使用Ember的事实;其思想适用于许多UI框架。)

数据容器模式(例如Vuex)

对于复杂的情况或亲子交流涉及太多中间人的情况,请使用Vuex或同等的数据容器技术。适当时,使用命名空间模块

例如,为具有许多互连的复杂组件集合(例如功能齐全的日历组件)创建单独的命名空间可能是合理的。

发布/订阅(事件总线)模式

如果事件总线(或“发布/订阅”)模式更适合您的需求,那么Vue核心团队现在建议使用第三方库(例如mitt)。(请参阅第1段中引用的RFC。)

奖金杂乱无章和代码

这是用于同级至同级通信的最低公共祖先解决方案的基本示例,通过游戏whack-a-mole进行了说明

幼稚的方法可能是认为“摩尔1应该告诉摩尔2在被重击后出现”。但是Vue不鼓励这种方法,因为它希望我们从树形结构的角度进行思考。

这可能是一件非常好的事情。如果没有某种计费系统(如Vuex提供的功能),节点在各个DOM树之间直接通信的非平凡应用程序将很难调试。最重要的是,使用“数据减少,事件增加”的组件往往表现出较低的耦合度和较高的可重用性,这都是非常有用的特性,可帮助大型应用程序扩展。

在此示例中,当一颗痣被重击时,它将发出一个事件。游戏管理器组件确定应用程序的新状态是什么,因此同胞mole鼠知道在Vue重新渲染后隐式做什么。这是一个微不足道的“最低共同祖先”示例。

Vue.component('whack-a-mole', {
  data() {
    return {
      stateOfMoles: [true, false, false],
      points: 0
    }
  },
  template: `<div>WHACK - A - MOLE!<br/>
    <a-mole :has-mole="stateOfMoles[0]" v-on:moleMashed="moleClicked(0)"/>
    <a-mole :has-mole="stateOfMoles[1]"  v-on:moleMashed="moleClicked(1)"/>
    <a-mole :has-mole="stateOfMoles[2]" v-on:moleMashed="moleClicked(2)"/>
    <p>Score: {{points}}</p>
</div>`,
  methods: {
    moleClicked(n) {
      if(this.stateOfMoles[n]) {
         this.points++;
         this.stateOfMoles[n] = false;
         this.stateOfMoles[Math.floor(Math.random() * 3)] = true;
      }   
    }
  }
})

Vue.component('a-mole', {
  props: ['hasMole'],
  template: `<button @click="$emit('moleMashed')">
      <span class="mole-button" v-if="hasMole">🐿</span><span class="mole-button" v-if="!hasMole">🕳</span>
    </button>`
})

var app = new Vue({
  el: '#app',
  data() {
    return { name: 'Vue' }
  }
})
.mole-button {
  font-size: 2em;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
  <whack-a-mole />
</div>

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.