151

概述

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

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


背景

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

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

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

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

[讨论链接]

和:

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

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


关注点

我担心的是:

  • 每个storeevent具有全局可见性(如果我错了,请纠正我);
  • 每次小交流都新建一个店铺太浪费了;

我想要的是兄弟组件的一些范围 events或可见性。stores(或者也许我不明白上面的想法。)


问题

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

4

7 回答 7

208

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

组件 1:

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

组件 2:

mounted() {
    this.$root.$on('eventing', data => {
        console.log(data);
    });
}
于 2017-10-29T19:14:36.780 回答
100

在 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 的答案,其中描述了一个更简单的解决方案。

于 2016-11-05T20:41:25.050 回答
57

状态范围

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

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

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

请注意,全局状态的一部分可能在某个时候最终进入本地状态,并且它可以像任何其他本地状态一样传递给子组件,无论是完全还是稀释以匹配用例。


沟通渠道

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

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

  • 全局状态
  • 亲子
  • 父母子女
  • 兄弟姐妹

不同的关注点与不同的沟通渠道有关。

道具:直接亲子

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

事件:直接子父

$emit$on。最简单的直接儿童与父母沟通的沟通渠道。事件启用 2-way 数据绑定。

提供/注入:全球或遥远的本地状态

在 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是 Vue.js 应用程序的状态管理模式 + 库。它充当应用程序中所有组件的集中存储,其规则确保状态只能以可预测的方式发生变化。

现在你问

[S]我应该为每个次要通信创建 vuex 存储吗?

在处理全局状态时它真的很出色,包括但不限于:

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

因此,您的组件可以真正专注于它们应该成为的东西,管理用户界面,而全局商店可以管理/使用一般业务逻辑并通过getteraction提供清晰的 API 。

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

为避免在全局状态下处理混乱的所有内容,请参阅应用程序结构建议。

参考和方法:边缘案例

尽管存在 props 和 events,但有时您可能仍需要直接访问 JavaScript 中的子组件。

它仅作为直接子操作的逃生舱口- 您应该避免$refs从模板或计算属性中访问。

如果您发现自己经常使用 refs 和子方法,那么可能是时候提升状态或考虑此处或其他答案中描述的其他方式了。

$parent: 边缘案例

与 类似$root,该$parent属性可用于从子级访问父级实例。这可能很容易成为使用道具传递数据的懒惰替代方案。

在大多数情况下,深入到父级会使您的应用程序更难调试和理解,尤其是当您更改父级中的数据时。稍后查看该组件时,将很难弄清楚该突变来自何处。

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

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

有关事件总线模式的最新信息,请参阅@AlexMA 的答案。

这是过去的模式,将 props 从远处传递到深度嵌套的子组件,中间几乎没有其他组件需要这些。谨慎使用精心挑选的数据。

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

下面演示了一个简单的错误如何导致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/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 。

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

  • 应用特定容器
  • 通用/展示组件

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

应用特定容器

注意:请参阅新的Composition API作为这些容器的替代方案。

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

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

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

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

通用/展示组件

这些应该从 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
                    });
            }
        }
    }
</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 和后端。

于 2018-04-07T01:34:07.357 回答
11

好的,我们可以使用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

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视图。
于 2016-08-10T14:29:27.180 回答
8

如何处理兄弟姐妹之间的沟通取决于情况。但首先我要强调的是,全局事件总线方法正在 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 的组件往往表现出低耦合性和高可重用性

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

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>

于 2020-03-27T22:43:28.403 回答
7

如果我想“破解”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而是使用类似的东西。

于 2016-07-28T03:47:32.520 回答
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>
于 2021-03-26T23:54:10.973 回答