Vue.js 2.0中兄弟组件之间的通信

概述

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

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


背景资料

据我所知,Vue.js 2.x 是兄弟通信 是使用商店或事件总线的首选方法。

根据 埃文(Vue.js 的创建者)的说法:

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

如果一段数据需要由多个组件共享,请选择 全球商店。

[ 链接到讨论]

还有:

不推荐使用 .once.sync。道具现在总是单向下行 在父范围内产生副作用时,组件需要 显式地 emit一个事件,而不是依赖于隐式绑定。

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


担忧

我担心的是:

  • 每个 storeevent都具有全球可见性(如果我错了请纠正我) ;
  • 为每个次要的交流创建一个新的商店太浪费了;

我想要的是一些 范围eventsstores的同级组件的可见性。(或许我并不理解上述观点。)


提问

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

73239 次浏览

如果我想“破解”Vue.js 中的正常通信模式,特别是现在 .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的东西。

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

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

让我们假设我们希望在单击 List中的某个元素时更新 Details组件。


Parent:

模板:

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

这里:

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

JavaScript:

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

JavaScript:

//...
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视图。

在 Vue.js 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,它描述了一个更简单的解决方案。

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

组成部分1:

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

组成部分2:

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

免责声明: 这个答案是很久以前写的,它可能不反映最新的 Vue 开发或趋势。对这个问题的答案持保留态度,如果你发现任何过时的、不再有效的或者没有帮助的东西,请评论。


州立监视器

在设计 Vue 应用程序(实际上是任何基于组件的应用程序)时,有不同类型的数据,这些数据取决于我们正在处理的关注点,而且每个关注点都有自己的首选通信通道。

  • 全局状态: 可能包括已登录的用户、当前主题等。

  • 本地状态: 表单属性、禁用按钮状态等。

请注意,全局状态的一部分可能在某个时刻以本地状态结束,并且它可以像任何其他本地状态一样传递给子组件,要么完全传递,要么稀释以匹配用例。


通讯频道

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

每个实现都针对一个特定的通信渠道,其中包括:

  • 全球状态
  • 亲子关系
  • 孩子的父母
  • 兄弟姐妹

不同的关注点涉及到不同的沟通渠道。

道具 : 直系亲子

Vue 中用于单向数据绑定的最简单的通信通道。

活动 : 直接子女-父母

重要告示: Vue 版本3中的 取出 ABC0和 $once

$emitv-on事件侦听器。最简单的直接子-父通信通道。事件支持双向数据绑定。

提供/注入 : 全局或远程本地状态

这是在 Vue 2.2 + 中添加的,与 React 的上下文 API 非常相似,它可以作为事件总线的可行替代品。

在组件树中的任何位置,组件 提供都可以访问一些数据,这些数据可以通过 inject组件的属性访问。

app.component('todo-list', {
// ...
provide() {
return {
todoLength: Vue.computed(() => this.todos.length)
}
}
})


app.component('todo-list-statistics', {
inject: ['todoLength'],
created() {
console.log(`Injected property: ${this.todoLength.value}`) // > Injected property: 5
}
})

这可以用来在应用程序的根目录中提供全局状态,或者在树的子集中提供本地化状态。

集中存储(全局状态)

注意: Vuex 5显然是 皮妮亚。请继续关注。(Tweet)

Vuex 是 Vue.js 应用程序的 状态管理模式 + 库。 中的所有组件的集中存储 应用程序,其规则确保状态只能在 一种可预测的时尚。

现在是 你问吧:

我是否应该为每个次要通信创建 vuex 存储?

在处理全局状态时,它确实闪耀着光芒,其中包括但不限于:

  • 从后端接收的数据,
  • 像主题一样的全局 UI 状态,
  • 任何数据持久层,例如保存到后端或与本地存储接口,
  • 祝酒词或通知,
  • 等等。

因此,您的组件可以真正专注于它们应该做的事情,管理用户界面,而全局存储可以管理/使用通用业务逻辑,并通过 [俄语]行动提供清晰的 API。

这并不意味着您不能将其用于组件逻辑,但是我个人会将该逻辑作用于名称空间的 Vuex 模块,只使用必要的全局 UI 状态。

为了避免在全球状态下处理一大堆乱七八糟的事情,请参阅 应用架构的建议。

参考文献 和方法: 边缘情况

尽管存在道具和事件,有时你可能仍然 需要直接访问 JavaScript 中的子组件。

它只意味着作为一个 逃生门的直接儿童操纵- 应该避免从模板或计算属性中访问 $refs

如果您发现自己经常使用引用和子方法,那么可能是时候使用 把国家举起来或考虑本文或其他答案中描述的其他方法了。

$parent : 边缘案例

$root类似,可以使用 $parent属性访问 从子实例中获取父实例 使用道具传递数据的惰性替代方法。

在大多数情况下,进入父级使您的应用程序更多 难以调试和理解,尤其是在 当稍后查看该组件时,它将非常 很难找出这种突变的来源。

实际上,您可以使用 $parent$ref$root导航整个树结构,但是这将类似于将所有内容都全局化,并且可能变成无法维护的意大利面条。

事件总线: 全局/远程本地状态

有关事件总线模式的最新信息,请参见 @ AlexMA 的回答

这是过去的模式传递道具到各地从远至深嵌套的儿童组件,几乎没有其他组件需要这些在两者之间。谨慎地使用精选的数据。

小心: 绑定自己到事件总线的组件的后续创建将被绑定不止一次——导致多个处理程序被触发和泄漏。在我过去设计的所有单页应用程序中,我个人从未感到需要活动总线。

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

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

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


组件类型

免责声明: 下面的 “容器”与“表示性”组件只是构建项目的一种方式,现在有多种选择,比如新的 组合 API可以有效地取代我在下面描述的“应用程序特定容器”。

为了协调所有这些通信,为了简化可重用性和测试,我们可以将组件看作两种不同的类型。

  • 应用程序特定容器
  • 通用/表示组件

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

应用程序特定容器

注意: 看到新的 组合 API作为这些容器的替代品。

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

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

同级组件的范围 以某种方式 eventsstores可见性

这就是范围界定的地方。大多数组件不知道存储,这个组件(大多数情况下)应该使用一个名称空间的存储模块,该模块的 gettersactions集有限,应用于提供的 烦人的装订助手

通用/表示组件

它们应该从道具接收数据,对自己的本地数据进行更改,并发出简单的事件。在大多数情况下,他们根本不应该知道 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
});
}
}
}
</script>

虽然对于一个简单的应用程序来说,它看起来不错,但是它也有很多缺点:

  • 显式使用全局 Axios 实例
  • UI 内部的硬编码 API
  • 紧密耦合到根组件(事件总线模式)
  • 做单元测试更难

关注点分离

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

按照以下模式,我们可以:

  • 简单 用单元测试测试每个关注点
  • 在不影响组件的情况下更改 API
  • 可以根据自己的喜好来配置 HTTP 通信(Axios、提取、添加中间件、测试等)
  • 在任何地方重用 输入组件(减少耦合)
  • 通过全局商店绑定对应用程序中任何地方的状态更改作出反应
  • 等等。

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

// 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.saveState,
])
}
</script>

因为 Vuex 存储 行动处理后端通信,所以我们的容器不需要知道 Axios 和后端。

如何处理兄弟姐妹之间的沟通取决于具体情况。但首先我想强调的是 在 Vue.js 中,全球事件总线方法正在消失。看这个 RFC。所以才有了这个答案。

最近公共祖先模式(或「 LCA 」)

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

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

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

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

要了解更多细节,我建议同事参考 这篇精彩的博客文章。(忽略其示例使用 Ember 的事实,其概念适用于许多框架)。

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

对于复杂的情况或父母与子女之间的沟通可能涉及太多中间人的情况,可以使用 Vuex 或相当的数据容器技术。

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

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

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

杂项

下面是一个用于兄弟姐妹之间通信的 LCA 解决方案的小例子(可能过于简单) ,这是一个名为 打地鼠的游戏。

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

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

在本例中,游戏管理器组件将鼹鼠可见性作为一个道具传递给鼹鼠子组件。当一个可见的鼹鼠被“重击”(点击)时,它会发出一个事件。游戏管理器组件(公共祖先)接收事件并修改其状态。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>

在我的例子中,我有一个带有可编辑单元格的表。当用户单击一个单元格到另一个单元格以编辑内容时,我只希望一次编辑一个单元格。 解决方案是使用父-子(道具)和子-父(事件)。 在下面的示例中,我循环访问一个包含“ rows”的数据集,并使用 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>