Javascript VueJs 2.0 中兄弟组件之间的通信

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/38616167/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-23 21:39:25  来源:igfitidea点击:

Communication between sibling components in VueJs 2.0

javascriptvue.jsvuejs2vue-componentvuex

提问by Sergei Panfilov

Overview

概述

In Vue.js 2.x, model.syncwill be deprecated.

在 Vue.js 2.x 中,model.sync将被弃用

So, what is a proper way to communicate between sibling components in Vue.js 2.x?

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



Background

背景

As I understand Vue 2.x, the preferred method for sibling communication is to use a store or an event bus.

据我了解 Vue 2.x,兄弟通信的首选方法是使用 store 或 event bus

According to Evan(creator of Vue):

根据埃文(Vue 的创始人)的说法:

It's also worth mentioning "passing data between components" is generally a bad idea, because in the end the data flow becomes untrackable and very hard to debug.

If a piece of data needs to be shared by multiple components, prefer global storesor Vuex.

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

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

[Link to discussion]

[讨论链接]

And:

和:

.onceand .syncare deprecated. Props are now always one-way down. To produce side effects in the parent scope, a component needs to explicitly emitan event instead of relying on implicit binding.

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

So, Evan suggestsusing $emit()and $on().

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



Concerns

顾虑

What worries me is:

让我担心的是:

  • Each storeand eventhas a global visibility (correct me if I'm wrong);
  • It's too wasteful to create a new store for each minor communication;
  • 每个storeevent都有全局可见性(如果我错了,请纠正我);
  • 每次次要的交流都新建一个店铺太浪费了;

What I want is to some scopeeventsor storesvisibility for siblings components. (Or perhaps I didn't understand the above idea.)

我想要的是兄弟组件的一些范围eventsstores可见性。(或者也许我不理解上述想法。)



Question

So, what is the correct way to communicate between sibling components?

那么,兄弟组件之间通信的正确方式是什么?

采纳答案by kakoni

With Vue 2.0, I'm using the eventHub mechanism as demonstrated in the documentation.

在 Vue 2.0 中,我使用了文档中演示的 eventHub 机制。

  1. Define centralized event hub.

    const eventHub = new Vue() // Single event hub
    
    // Distribute to components using global mixin
    Vue.mixin({
        data: function () {
            return {
                eventHub: eventHub
            }
        }
    })
    
  2. Now in your component you can emit events with

    this.eventHub.$emit('update', data)
    
  3. And to listen you do

    this.eventHub.$on('update', data => {
    // do your thing
    })
    
  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
    })
    

UpdatePlease see the answer by @alex, which describes a simpler solution.

更新请参阅@alex的回答,它描述了一个更简单的解决方案。

回答by Alex

You can even make it shorter and use rootVueinstance as global Event Hub:

您甚至可以缩短它并使用Vue实例作为全局事件中心:

Component 1:

组件 1:

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

Component 2:

组件 2:

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

回答by Emile Bergeron

Communication types

通讯类型

When designing a Vue application (or in fact, any component based application), there are different communication types that depend on which concerns we're dealing with and they have their own communication channels.

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

Business logic:refers to everything specific to your app and its goal.

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

Presentation logic:anything that the user interacts with or that results from the interaction from the user.

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

These two concerns are related to these types of communication:

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

  • Application state
  • Parent-child
  • Child-parent
  • Siblings
  • 应用状态
  • 亲子
  • 父子
  • 兄弟姐妹

Each type should use the right communication channel.

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



Communication channels

沟通渠道

A channel is a loose term I'll be using to refer to concrete implementations to exchange data around a Vue app.

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

Props: Parent-Child presentation logic

道具:父子呈现逻辑

The simplest communication channel in Vue for direct Parent-Childcommunication. It should mostly be used to pass data relating to presentation logic or a restricted set of data down the hierarchy.

Vue 中最简单的直接父子通信的通信通道。它主要用于传递与表示逻辑相关的数据或沿层次结构向下传递的一组受限数据。

Refs and methods: Presentation anti-pattern

参考文献和方法:表示反模式

When it doesn't make sense to use a prop to let a child handle an event from a parent, setting up a refon the child component and calling its methodsis just fine.

当使用 prop 让子组件处理来自父组件的事件没有意义时,在子组件上设置 aref并调用其方法就好了。

Don't do that, it's an anti-pattern. Rethink your component architecture and data flow. If you find yourself wanting to call a method on a child component from a parent, it's probably time to lift the state upor consider the other ways described here or in the other answers.

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

Events: Child-Parent presentation logic

事件:Child-Parent 呈现逻辑

$emitand $on. The simplest communication channel for direct Child-Parent communication. Again, should be used for presentation logic.

$emit$on。最简单的直接父子沟通的沟通渠道。同样,应该用于表示逻辑。

Event bus

事件总线

Most answers give good alternatives for event bus, which is one of the communication channels available for distant components, or anything in fact.

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

This can become useful when passing props all over the place from far up down to deeply nested children components, with almost no other components needing these in between. Use sparingly for carefully selected data.

当将 props 从上到下传递到深层嵌套的子组件时,这会变得很有用,几乎没有其他组件需要它们。谨慎使用精心挑选的数据。

Be careful:Subsequent creation of components that are binding themselves to the event bus will be bound more than once--leading to multiple handlers triggered and leaks. I personally never felt the need for an event bus in all the single page apps I've designed in the past.

小心:随后创建的将自身绑定到事件总线的组件将被多次绑定——导致多个处理程序被触发和泄漏。我个人从未觉得在我过去设计的所有单页应用程序中都需要事件总线。

The following demonstrates how a simple mistake leads to a leak where the Itemcomponent still triggers even if removed from the DOM.

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

// A component that binds to a custom 'update' event.
var Item = {
  template: `<li>{{text}}</li>`,
  props: {
    text: Number
  },
  mounted() {
    this.$root.$on('update', () => {
      console.log(this.text, 'is still alive');
    });
  },
};

// Component that emits events
var List = new Vue({
  el: '#app',
  components: {
    Item
  },
  data: {
    items: [1, 2, 3, 4]
  },
  updated() {
    this.$root.$emit('update');
  },
  methods: {
    onRemove() {
      console.log('slice');
      this.items = this.items.slice(0, -1);
    }
  }
});
<script src="https://unpkg.com/[email protected]/dist/vue.min.js"></script>

<div id="app">
  <button type="button" @click="onRemove">Remove</button>
  <ul>
    <item v-for="item in items" :key="item" :text="item"></item>
  </ul>
</div>

Remember to remove listeners in the destroyedlifecycle hook.

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

Centralized store (Business logic)

集中存储(业务逻辑)

Vuexis the way to go with Vue for state management. It offers a lot more than just events and it's ready for full scale application.

Vuex是与 Vue 一起进行状态管理的方式。它提供的不仅仅是事件,而且已经为全面应用做好了准备。

And now you ask:

现在你问

[S]hould I create vuex's store for each minor communication?

[S]我应该为每个次要的交流创建 vuex 的商店吗?

It really shines when:

在以下情况下它真的很闪耀:

  • dealing with your business logic,
  • communicating with a backend (or any data persistence layer, like local storage)
  • 处理您的业务逻辑,
  • 与后端(或任何数据持久层,如本地存储)通信

So your components can really focus on the things they're meant to be, managing user interfaces.

所以你的组件可以真正专注于它们应该做的事情,管理用户界面。

It doesn't mean that you can't use it for component logic, but I would scope that logic to a namespaced Vuex module with only the necessary global UI state.

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

To avoid dealing with a big mess of everything in a global state, the store should be separated in multiple namespaced modules.

为了避免在全局状态下处理混乱的所有内容,应该将存储分隔在多个命名空间模块中。



Component types

组件类型

To orchestrates all these communications and to ease re-usability, we should think of components as two different types.

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

  • App specific containers
  • Generic components
  • 应用程序特定容器
  • 通用组件

Again, it doesn't mean that a generic component should be reused or that an app specific container can't be reused, but they have different responsibilities.

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

App specific containers

应用程序特定容器

These are just simple Vue component that wraps other Vue components (generic or other app specific containers). This is where the Vuex store communication should happen and this container should communicate through other simpler means like props and event listeners.

这些只是包装其他 Vue 组件(通用或其他应用程序特定容器)的简单 Vue 组件。这是 Vuex store 通信应该发生的地方,这个容器应该通过其他更简单的方式进行通信,比如 props 和事件监听器。

These containers could even have no native DOM elements at all and let the generic components deal with the templating and user interactions.

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

scopesomehow eventsor storesvisibility for siblings components

以某种方式作用域eventsstores兄弟组件的可见性

This is where the scoping happens. Most components don't know about the store and this component should (mostly) use one namespaced store module with a limited set of gettersand actionsapplied with the provided Vuex binding helpers.

这就是范围界定发生的地方。大多数组件不知道 store 并且这个组件应该(大部分)使用一个命名空间的 store 模块,其中包含一组有限的gettersactions应用提供的Vuex binding helpers

Generic components

通用组件

These should receive their data from props, make changes on their own local data, and emit simple events. Most of the time, they should not know a Vuex store exists at all.

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

They could also be called containers as their sole responsibility could be to dispatch to other UI components.

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



Sibling communication

兄弟姐妹交流

So, after all this, how should we communicate between two sibling components?

那么,毕竟这一切,我们应该如何在两个兄弟组件之间进行通信?

It's easier to understand with an example: say we have an input box and its data should be shared across the app (siblings at different places in the tree) and persisted with a backend.

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

Starting with the worst case scenario, our component would mix presentationand businesslogic.

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

// 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>

To separate these two concerns, we should wrap our component in an app specific container and keep the presentation logic into our generic input component.

为了分离这两个关注点,我们应该将我们的组件包装在一个特定于应用程序的容器中,并将呈现逻辑保留在我们的通用输入组件中。

Our input component is now reusable and doesn't know about the backend nor the siblings.

我们的输入组件现在是可重用的,并且不知道后端和兄弟组件。

// 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>

Our app specific container can now be the bridge between the business logic and the presentation communication.

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

// 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>

Since the Vuex store actionsdeal with the backend communication, our container here doesn't need to know about axios and the backend.

由于 Vuex 存储操作处理后端通信,我们这里的容器不需要知道 axios 和后端。

回答by Sergei Panfilov

Okay, we can communicate between siblings via parent using v-onevents.

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

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

Let's assume that we want update Detailscomponent when we click some element in List.

假设我们想要Details在单击 中的某个元素时更新组件List



in Parent:

Parent

Template:

模板:

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

Here:

这里:

  • v-on:select-itemit's an event, that will be called in Listcomponent (see below);
  • setSelectedItemit's a Parent's method to update selectedModel;
  • v-on:select-item这是一个事件,将在List组件中调用(见下文);
  • setSelectedItem这是 aParent的更新方法selectedModel

JS:

JS:

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


In List:

List

Template:

模板:

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

JS:

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"
  },
}
//...

Here:

这里:

  • this.$emit('select-item', item)will send item via select-itemdirectly in parent. And parent will send it to the Detailsview
  • this.$emit('select-item', item)将通过select-item直接在父项中发送项目。并且父母将其发送到Details视图

回答by Hector Lorenzo

What I usually do if I want to "hack" the normal patterns of communication in Vue, specially now that .syncis deprecated, is to create a simple EventEmitter that handles communication between components. From one of my latest projects:

如果我想“破解”Vue 中的正常通信模式,特别是现在.sync已弃用,我通常会做的是创建一个简单的 EventEmitter 来处理组件之间的通信。从我的最新项目之一:

import {EventEmitter} from 'events'

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

With this Transmitterobject you can then do, in any component:

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

import Transmitter from './Transmitter'

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

And to create a "receiving" component:

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

import Transmitter from './Transmitter'

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

Again, this is for really specific uses. Don't base your whole application on this pattern, use something like Vuexinstead.

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

回答by AlexMA

How to handle communication between siblings depends on the situation. But first I want to emphasize that the global event bus approach is going away in Vue 3. See this RFC. Hence why I decided to write a new answer.

如何处理兄弟姐妹之间的沟通取决于情况。但首先我想强调的是,全局事件总线方法正在 Vue 3 中消失。请参阅此RFC。因此,我决定写一个新答案。

Lowest Common Ancestor Pattern (or “LCA”)

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

For simple cases, I strongly recommend using the Lowest Common Ancestor pattern (also known as “data down, events up”). This pattern is easy to read, implement, test, and debug.

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

In essence, this means if two components need to communicate, put their shared state in the closest component that both share as an ancestor. Pass data from parent component to child component via props, and pass information from child to parent by emitting an event (see example of this at the bottom of this answer).

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

For a contrived example, in an email app, if the "To" component needed to interact with the "message body" component, the state of that interaction could live in their parent (maybe a component called email-form). You might have a prop in the email-formcalled addresseeso that the message body can automatically prepend Dear {{addressee.name}}to the email based on the receiver's email address.

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

LCA becomes onerous if communication has to travel long distances with lots of middlemen components. I often refer colleagues to this excellent blog post. (Ignore the fact that its examples use Ember; its ideas are applicable across many UI frameworks.)

如果通信必须通过大量中间商组件进行长距离传输,LCA 就会变得繁重。我经常向同事推荐这篇优秀的博客文章。(忽略它的示例使用 Ember 的事实;它的想法适用于许多 UI 框架。)

Data Container Pattern (e.g., Vuex)

数据容器模式(例如,Vuex)

For complex cases or situations where parent–child communication would involve too many middlemen, use Vuex or an equivalent data container technology. When appropriate, use namespaced modules.

对于复杂的案例或亲子通信会涉及太多中间人的情况,请使用 Vuex 或等效的数据容器技术。在适当的时候,使用命名空间模块

For example, it might be reasonable to create a separate namespace for a complex collection of components with many interconnections, such as a fully-featured calendar component.

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

Publish/Subscribe (Event Bus) Pattern

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

If the event bus (or “publish/subscribe”) pattern is more appropriate for your needs, the Vue core team now recommends using a third party library such as mitt. (See the RFC referenced in paragraph 1.)

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

Bonus ramblings and code

奖金漫谈和代码

Here is a basic example of Lowest Common Ancestor solution for sibling-to-sibling communication, illustrated via the game whack-a-mole.

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

A na?ve approach might be to think, “mole 1 should tell mole 2 to appear after it is whacked”. But Vue discourages this sort of approach, since it wants us to think in terms of tree structures.

一种天真的方法可能是认为,“鼹鼠 1 应该告诉鼹鼠 2 在它被击打后出现”。但是 Vue 不鼓励这种方法,因为它希望我们从树结构的角度来思考。

This is likely a very good thing. A non-trivial app where nodes communicate directly to each-other across DOM trees would be very difficult to debug without some sort of accounting system (like Vuex provides). On top of that, components that use “data down, events up” tend to exhibit low coupling and high re-usability—both highly desirable traits that help large apps scale.

这很可能是一件非常好的事情。如果没有某种记帐系统(如 Vuex 提供),节点之间直接通过 DOM 树相互通信的非平凡应用程序将很难调试。最重要的是,使用“数据向下,事件向上”的组件往往表现出低耦合和高可重用性——这两种特性都有助于大型应用程序的扩展。

In this example, when a mole is whacked, it emits an event. The game manager component decides what the new state of the app is, and thus the sibling mole knows what to do implicitly after Vue re-renders. It's a somewhat trivial “lowest common ancestor” example.

在这个例子中,当一只鼹鼠被击打时,它会发出一个事件。游戏管理器组件决定应用程序的新状态,因此兄弟鼹鼠知道在 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>