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

Ser*_*lov 88 javascript vue.js vue-component vuex vuejs2

在vuejs 2.0 model.sync中将被弃用.

那么,在vuejs 2.0中兄弟组件之间进行通信的正确方法是什么?

我在Vue 2.0中发现的想法通过使用商店或事件总线进行兄弟姐妹的沟通.

埃文说:

值得一提的是"在组件之间传递数据"通常是一个坏主意,因为最终数据流变得无法跟踪并且很难调试.

如果一个数据需要由多个组件共享,则更喜欢 全局存储Vuex.

[ 链接到讨论 ]

和:

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

(所以,他建议使用$emit$on)

我很担心因为:

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

我想要的是以某种方式或兄弟姐妹组件的可见性范围.或者也许我没有抓住这个想法.eventsstores

那么,如何以正确的方式沟通?

Ale*_*lex 105

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

第1部分:

this.$root.$emit('eventing', data);
Run Code Online (Sandbox Code Playgroud)

第2部分:

mounted() {
    this.$root.$on('eventing', data => {
        console.log(data);
    });
}
Run Code Online (Sandbox Code Playgroud)

  • 这是完美,优秀和非常简洁的解决方案. (8认同)
  • 如果您只想直接与兄弟姐妹交流,请使用 $parent 而不是 $root (5认同)
  • 这比定义附加事件中心并将其附加到任何事件消费者更有效。 (3认同)
  • 我是这个解决方案的忠实粉丝,因为我真的不喜欢有范围的事件。但是,我并不是每天都使用 VueJS,所以我很好奇是否有人看到这种方法的问题。 (3认同)
  • 所有答案中最简单的解决方案 (3认同)
  • 不错,简短且易于实现,也易于理解 (2认同)
  • 这位天才怎么样?如果我错了,请纠正我,但它似乎具有依赖全局存储的大部分缺点(除非对变量名称进行硬编码),并且没有将这些事件包含在相关组件中的优点。使用显式事件存储的想法是您可以选择哪个组件负责封装通信。例如,如果您有一个复杂的材料设计选择列表,则需要共享一些内部尺寸信息。 (2认同)

kak*_*oni 76

使用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
            }
        }
    })
    
    Run Code Online (Sandbox Code Playgroud)
  2. 现在在您的组件中,您可以发布事件

    this.eventHub.$emit('update', data)
    
    Run Code Online (Sandbox Code Playgroud)
  3. 并且倾听你的意思

    this.eventHub.$on('update', data => {
    // do your thing
    })
    
    Run Code Online (Sandbox Code Playgroud)

更新 请参阅@alex的anwer,它描述了更简单的解决方案.

  • 为了将来参考,请不要用其他人的答案更新您的答案(即使您认为它更好并且您参考了它)。链接到备用答案,或者如果您认为他们应该接受另一个答案,甚至可以要求 OP 接受另一个答案-但是将他们的答案复制到您自己的答案中是不好的形式,并且不鼓励用户在到期时给予信用,因为他们可能只是简单地只支持您的只回答。鼓励他们通过不包含在您自己的答案中来导航到(并因此赞成)您所引用的答案。 (6认同)
  • 一个更简单的解决方案是使用@Alex所描述的 - 这个.$ root.$ emit()`和`this.$ root.$ on on()` (5认同)
  • 请注意,Vue 3 将不再支持此解决方案。请参阅 /sf/answers/4262655351/ (4认同)
  • 只是抬头:密切注意Global Mixins,并尽可能避免使用它们,因为根据这个链接https://vuejs.org/v2/guide/mixins.html#Global-Mixin他们可能会影响甚至第三方COMPONENTES. (3认同)
  • 感谢您提供宝贵的反馈@GrayedFox,并相应地更新了我的答案。 (2认同)

Emi*_*ron 38

我知道这是一个老问题,但我想揭露其他沟通渠道以及如何从更高的角度查看应用和通信.


沟通类型

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

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

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

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

  • 申请状态
  • 亲子
  • 父子
  • 兄弟姐妹

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


沟通渠道

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

道具(演示逻辑)

Vue中最简单的通信通道,用于直接的父子通信.它主要用于传递与表示逻辑有关的数据或在层次结构中传递受限制的数据集.

参考和方法(演示逻辑)

当使用prop让子进程处理来自父进程的事件没有意义时,在子组件上设置一个ref并调用它的方法就没问题了.

有些人可能会说这是父母和孩子之间的紧密耦合,但它与使用道具的耦合相同.如果我们可以就道具合同达成一致,我们也可以就方法合同达成一致.

事件(演示逻辑)

$emit$on.直接的儿童 - 家长沟通的最简单的沟通渠道.同样,应该用于表示逻辑.

活动巴士(两者)

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

当将道具从远处向上传递到深层嵌套的子组件时,这可以变得有用,几乎没有其他组件需要这些组件.

注意:后续创建绑定到事件总线的组件将被绑定多次 - 导致多个处理程序被触发和泄漏.在我过去设计的所有单页应用程序中,我个人从未觉得需要事件总线.

下面演示了一个简单的错误如何导致泄漏,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);
    }
  }
});
Run Code Online (Sandbox Code Playgroud)
<script src="https://unpkg.com/vue@2.5.17/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>
Run Code Online (Sandbox Code Playgroud)

请记住在destroyed生命周期钩子中删除侦听器.

集中存储(业务逻辑)

Vuex是Vue用于状态管理的方式.它提供的不仅仅是事件,它已经准备好进行全面的应用.

现在你问:

我应该为每次小型沟通创建vuex的商店吗?

它真的很棒:

  • 处理您的业务逻辑,
  • 与后端通信

因此,您的组件可以真正专注于他们想要的事情,管理用户界面.

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

为了避免处理全局状态中的所有内容,我们应该将存储拆分为多个命名空间模块.


组件类型

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

  • 应用特定容器
  • 通用组件

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

应用特定容器

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

这些容器甚至可以根本没有本机DOM元素,让通用组件处理这个问题.

范围以某种方式eventsstores兄弟姐妹组件的可见性

这是范围发生的地方.大多数组件都不知道存储,这个组件应该(大多数)使用一个命名空间的存储模块和一组有限的应用程序,gettersactions使用提供的Vuex映射器.

通用组件

这些应该从props接收数据,对自己的本地数据进行更改,并发出简单的事件.大多数时候,他们不应该知道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>
Run Code Online (Sandbox Code Playgroud)

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

我们的输入组件现在可以重复使用,并且不了解后端和兄弟姐妹.

// 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>
Run Code Online (Sandbox Code Playgroud)

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

// 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>
Run Code Online (Sandbox Code Playgroud)

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

  • 同意关于方法的评论"_与使用props_相同的方法" (2认同)

Ser*_*lov 10

好的,我们可以通过父母使用v-on事件在兄弟姐妹之间进行交流.

Parent
 |-List of items //sibling 1 - "List"
 |-Details of selected item //sibling 2 - "Details"
Run Code Online (Sandbox Code Playgroud)

我们假设我们Details在点击某个元素时需要更新组件List.


Parent:

模板:

<list v-model="listModel"
      v-on:select-item="setSelectedItem" 
></list> 
<details v-model="selectedModel"></details>
Run Code Online (Sandbox Code Playgroud)

这里:

  • 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
  },
}
//...
Run Code Online (Sandbox Code Playgroud)

List:

模板:

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

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"
  },
}
//...
Run Code Online (Sandbox Code Playgroud)

这里:

  • this.$emit('select-item', item)select-item直接通过父母发送项目.父母将把它发送到Details视图


Ale*_*xMA 7

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

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

在大多数情况下,我建议使用最低共同祖先模式(也称为“数据下降,事件上升”)。这种模式易于阅读、实现、测试和调试。它还创建了一个优雅、简单的数据流。

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

例如,一个人可能有一个电子邮件应用程序:地址组件需要将数据传递给消息正文组件(可能是为了预填充“Hello <name>”),因此它们使用最接近的共享祖先(可能是电子邮件表单组件)保存收件人数据。

如果事件和道具需要通过许多“中间人”组件,LCA 可能会很烦人。

有关更多详细信息,我向同事推荐这篇出色的博客文章。(忽略它的示例使用 Ember 的事实,它的概念适用于许多框架)。

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

对于复杂的案例或亲子通信会涉及太多中间人的情况,请使用 Vuex 或等效的数据容器技术。

当单个商店变得过于复杂或杂乱无章时,请使用命名空间模块。例如,为具有许多互连的复杂组件集合(例如复杂日历)创建单独的命名空间可能是合理的。

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

如果事件总线(即发布/订阅)模式对您的应用程序更有意义(从架构的角度来看),或者您需要从现有的 Vue.js 应用程序中删除 Vue.js 的全局事件总线,Vue.js 核心团队现在建议使用第三方库,例如mitt。(请参阅第 1 段中引用的 RFC。)。

各种各样的

这是用于兄弟姐妹通信的 LCA 解决方案的一个小示例(可能过于简单)。这是一款名为whack-a-mole的游戏。

在这个游戏中,玩家在“敲打”一只鼹鼠时会获得分数,这会导致它隐藏,然后另一只鼹鼠出现在随机位置。为了构建这个包含“鼹鼠”组件的应用程序,人们可能会想,“鼹鼠组件 N 应该告诉鼹鼠组件 Y 在它被敲打后出现”。但是 Vue.js 不鼓励这种组件通信方法,因为 Vue.js 应用程序(和 html)实际上是树数据结构

这大概是件好事。一个大型/复杂的应用程序,其中节点在没有任何集中管理器的情况下相互通信,可能很难调试。此外,使用 LCA 的组件往往表现出低耦合和高可重用性

在这个例子中,游戏管理器组件将鼹鼠可见性作为道具传递给鼹鼠子组件。当一个可见的痣被“重击”(点击)时,它会发出一个事件。游戏管理器组件(公共祖先)接收事件并修改其状态。Vue.js 会自动更新 props,所以所有的鼹鼠组件都会收到新的可见性数据。

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' }
  }
})
Run Code Online (Sandbox Code Playgroud)
.mole-button {
  font-size: 2em;
}
Run Code Online (Sandbox Code Playgroud)
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
  <whack-a-mole />
</div>
Run Code Online (Sandbox Code Playgroud)


Hec*_*nzo 5

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

import {EventEmitter} from 'events'

var Transmitter = Object.assign({}, EventEmitter.prototype, { /* ... */ })
Run Code Online (Sandbox Code Playgroud)

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

import Transmitter from './Transmitter'

var ComponentOne = Vue.extend({
  methods: {
    transmit: Transmitter.emit('update')
  }
})
Run Code Online (Sandbox Code Playgroud)

并创建一个"接收"组件:

import Transmitter from './Transmitter'

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

同样,这是真正特定的用途.不要将整个应用程序基于此模式,Vuex而是使用类似的东西.

  • 实际上我不同意我们需要为每个次要的通信使用 vuex ...... (2认同)