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

时间:2016-07-27 14:40:25

标签: javascript vue.js vuejs2 vue-component vuex

在vuejs 2.0 model.sync中将deprecated

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

当我在Vue 2.0中抓住想法时,通过使用商店或事件总线来实现兄弟通信

根据evan

  

值得一提的是,在组件之间传递数据"是   通常是一个坏主意,因为最终数据流变成了   无法检查,很难调试。

     

如果一个数据需要由多个组件共享,则更喜欢   global storesVuex

[Link to discussion]

  

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

(所以,他suggest将使用$emit$on

我担心因为:

  • 每个storeevent都具有全局可见性(如果我错了,请更正我);
  • 为每次次要沟通创建一个新商店非常重要;

我想要的是范围某种方式eventsstores兄弟姐妹组件的可见性。或许我没有理解这个想法。

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

7 个答案:

答案 0 :(得分:75)

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

组件1:

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

组件2:

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

答案 1 :(得分:67)

使用Vue 2.0,我正在使用documentation中演示的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
    })
    
  4. <强>更新 请通过@alex查看答案,其中介绍了更简单的解决方案。

答案 2 :(得分:32)

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

通讯类型

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

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

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

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

  • 申请状态
  • 亲子
  • 子父
  • 邻近

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

沟通渠道

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

道具(演示逻辑)

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

参考和方法(演示逻辑)

如果使用道具让孩子处理来自父母的事件没有意义,setting up a ref on the child component and calling its methods就可以了。

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

事件(演示逻辑)

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

活动巴士(两者)

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

当将道具从远处向下传递到深层嵌套的子组件时,这很有用,几乎没有其他组件需要这些组件。

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

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

&#13;
&#13;
// 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);
    }
  }
});
&#13;
<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>
&#13;
&#13;
&#13;

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

集中存储(业务逻辑)

Vuex是Vue用于州管理的方式。它提供的不仅仅是活动,而且可以全面应用。

现在you ask

  

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

在以下情况下真的很棒:

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

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

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

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

组件类型

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

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

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

应用特定容器

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

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

  

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

这是范围发生的地方。大多数组件都不了解商店,这个组件应该(大部分)使用一个命名空间的商店模块,并使用提供的Vuex映射器应用一组有限的gettersactions

通用组件

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

它们也可以被称为容器,因为它们的唯一责任可能是分发给其他UI组件。

兄弟姐妹沟通

所以,在这之后,我们应该如何在两个兄弟组件之间进行通信?

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

最坏情况开始,我们的组件会混合使用 presentation business 逻辑。

// 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 :(得分: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视图

答案 4 :(得分:5)

如果我想要&#34; hack&#34;我通常会做什么? 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')
  }
})

创建一个&#34;接收&#34;成分:

import Transmitter from './Transmitter'

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

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

答案 5 :(得分:3)

如何处理兄弟姐妹之间的通信取决于情况。但首先,我想强调的是,Vue.js 3中的 全局事件总线方法已经消失 。参见此RFC。因此,为什么我决定写一个新答案。

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

对于简单情况,我强烈建议使用lowest common ancestor模式(也称为“数据关闭,事件增加”)。这种模式易于读取,实施,测试和调试。

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

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

如果通信必须与许多中间人一起进行很长距离的传输,LCA将变得很繁重。我经常推荐同事去this excellent blog post。 (忽略它的示例使用Ember的事实;它的思想适用于许多UI框架。)

数据容器模式(例如Vuex)

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

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

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

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

奖金乱码和代码

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

天真的想法可能是:“摩尔1应该告诉摩尔2在被重击后出现”。但是Vue.js不鼓励这种方法,因为它希望我们从tree structures的角度进行思考。

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

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

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>

答案 6 :(得分:0)

就我而言,我有一个带有可编辑单元格的表格。当用户从一个单元格单击另一个单元格以编辑内容时,我只希望一次可编辑一个单元格。 解决方案是使用父子(道具)和子父(事件)。 在下面的示例中,我循环遍历“行”数据集并使用 rowIndex 和 cellIndex 为每个单元格创建唯一(坐标)标识符。当一个单元格被点击时,一个事件从子元素触发到父元素,告诉父元素哪个坐标被点击。然后父组件设置 selectedCoord 并将其传递回子组件。所以每个子组件都知道自己的坐标和选定的坐标。然后它可以决定是否使自己可编辑。

<!-- PARENT COMPONENT -->
<template>
<table>
    <tr v-for="(row, rowIndex) in rows">
        <editable-cell
            v-for="(cell, cellIndex) in row"
            :key="cellIndex"
            :cell-content="cell"
            :coords="rowIndex+'-'+cellIndex"
            :selected-coords="selectedCoords"
            @select-coords="selectCoords"
        ></editable-cell>
    </tr>
</table>
</template>
<script>
export default {
    name: 'TableComponent'
    data() {
        return {
            selectedCoords: '',
        }
    },
    methods: {
        selectCoords(coords) {
            this.selectedCoords = coords;
        },
    },
</script>

<!-- CHILD COMPONENT -->
<template>
    <td @click="toggleSelect">
        <input v-if="coords===selectedCoords" type="text" :value="cellContent" />
        <span v-else>{{ cellContent }}</span>
    </td>
</template>
<script>
export default {
    name: 'EditableCell',
    props: {
        cellContent: {
            required: true
        },
        coords: {
            type: String,
            required: true
        },
        selectedCoords: {
            type: String,
            required: true
        },
    },
    methods: {
        toggleSelect() {
            const arg = (this.coords === this.selectedCoords) ? '' : this.coords;
            this.$emit('select-coords', arg);
        },
    }
};
</script>