Vue.js 2.0中同级组件之间的通信

ukdjmx9f  于 2022-11-17  发布在  Vue.js
关注(0)|答案(7)|浏览(161)

概述

在Vue.js 2.x中,model.sync将被弃用。
那么,在Vue.js 2.x中的同级组件之间进行通信的正确方式是什么呢?
背景
根据我对Vue.js 2.x的理解,兄弟通信的首选方法是使用存储或事件总线
根据Evan(Vue.js的创建者):
还值得一提的是,“在组件之间传递数据”通常是一个坏主意,因为最终数据流变得不可跟踪,很难调试。
如果一段数据需要由多个组件共享,则首选global storesVuex
[Link to discussion]我的天啊
还有:
不推荐使用.once.sync.属性现在始终单向关闭.若要在父作用域中产生副作用,组件需要显式emit事件,而不是依赖隐式绑定.
因此,Evan建议使用$emit()$on()

问题

我担心的是:

  • 每个storeevent都具有全局可见性(如果我错了,请纠正我);
  • 为每一个次要的通信创建一个新的存储区太浪费了;

我想要的是一些scopeeventsstores对兄弟组件的可见性。(或者也许我没有理解上面的想法。)

问题

那么,在同级组件之间进行通信的正确方式是什么呢?

e5nqia27

e5nqia271#

您甚至可以将其缩短,并使用根Vue示例作为全局事件集线器:
第一部分:

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

第二部分:

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

y3bcpkx12#

在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
         }
     }
 })

1.现在,在组件中,您可以使用

this.eventHub.$emit('update', data)

1.你要听

this.eventHub.$on('update', data => {
 // do your thing
 })

更新

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

vbopmzt1

vbopmzt13#

***免责声明:*此答案是很久以前写的,可能无法反映最新的Vue开发或趋势。请对此答案中的所有内容持保留态度,如果您发现任何过时、不再有效或无用的内容,请进行评论。

状态范围

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

***全局状态:**可能包括登录用户、当前主题等。
***本地状态:**表单属性、禁用按钮状态等。

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

通信信道

通道是一个松散的术语,我将使用它来指代围绕Vue应用程序交换数据的具体实现。
每种实现都针对特定的通信通道,包括:

  • 全局状态
  • 父子
  • 亲子
  • 同层级

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

Props:我的天啊!直接父子

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

Events:直接子-父

***重要通知:*Vue版本3中的$on and $once were removed

$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显然将是Pinia。请继续关注。(Tweet)*

Vuex是一个状态管理模式+库,用于Vue.js应用程序。它作为应用程序中所有组件的集中存储,并具有确保状态只能以可预测的方式变化的规则。
而现在你问:
我是否应该为每个次要通信创建vuex存储?
在处理全局状态时,它确实大放异彩,这包括但不限于:

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

因此,您的组件可以真正专注于它们应该做的事情,管理用户界面,而全局存储可以管理/使用常规业务逻辑,并通过gettersactions提供清晰的API。
这并不意味着您不能将其用于组件逻辑,但我个人认为该逻辑的范围是一个命名空间的Vuex module,其中只有必要的全局UI状态。

引用和方法:边缘案例

尽管存在道具和事件,有时您可能仍然需要直接访问JavaScript中的子组件。
它仅用作直接子操作的转义影线-应避免从模板或计算属性中访问$refs
如果您发现自己经常使用refs和child方法,那么可能是时候进行lift the state up或考虑这里或其他答案中描述的其他方法了。

$parent:边缘案例

$root类似,$parent属性可用于从子级访问父级示例。这可能是一个吸引人的方法,作为用prop传递数据的一种惰性替代方法。
在大多数情况下,进入父组件会使应用程序更难调试和理解,特别是在父组件中的数据发生了变化的情况下。当以后查看该组件时,很难找出变化的来源。
实际上,您可以使用$parent$ref$root来导航整个树结构,但这类似于让所有内容都是全局的,并且很可能变成不可维护的意大利面条。

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

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

这是过去的模式,从上到下到处传递道具到嵌套很深的子组件,几乎没有其他组件需要这些。对于精心选择的数据,请谨慎使用。

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

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

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

组件类型

***免责声明:*下面的"containers" versus "presentational" components只是构建项目的一种方法,现在有多种替代方法,比如新的Composition API,它可以有效地取代我在下面描述的“应用程序特定容器”。

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

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

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

应用程序特定容器

***注意:*请参阅新的组合API作为这些容器的替代。

这些只是 Package 其他Vue组件(通用或其他应用程序特定容器)的简单Vue组件。这是Vuex商店通信应该发生的地方,该容器应该通过其他更简单的方式(如道具和事件侦听器)进行通信。
这些容器甚至可以完全不包含本地DOM元素,而让通用组件处理模板和用户交互。

作用域以某种方式对同级组件显示eventsstores

大多数组件并不知道存储区的存在,这个组件应该(主要)使用一个命名空间存储区模块,并通过提供的Vuex绑定助手应用一组有限的gettersactions

通用/表示组件

它们应该从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
  • 与根组件紧密耦合(事件总线模式)
  • 更难进行单元测试

关注点分离

为了区分这两个问题,我们应该将组件 Package 在应用程序特定的容器中,并将表示逻辑保存在通用输入组件中。
使用以下模式,我们可以:

  • 轻松test each concern with unit tests
  • 在不影响组件的情况下更改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 store actions 处理后端通信,因此这里的容器不需要了解axios和后端。

ffx8fchx

ffx8fchx4#

好的,我们可以通过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视图。
qvk1mo1f

qvk1mo1f5#

如何处理兄弟之间的通信取决于具体情况。但首先我要强调的是,全局事件总线方法在Vue.js 3中将消失。请参见RFC。因此,有了这个答案。

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

大多数情况下,我建议使用lowest common ancestor模式(也称为“数据向下,事件向上”)。这种模式易于阅读、实现、测试和调试。它还创建了一个优雅、简单的数据流。
本质上,这意味着如果两个组件需要通信,则将它们的共享状态放在最近的组件中,这两个组件共享为祖先。通过属性将数据从父组件传递到子组件,并通过发出事件将信息从子组件传递到父组件(下面的示例代码)。
例如,您可能有一个电子邮件应用程序:地址组件需要与消息正文组件进行数据通信(可能是为了预先填充“Hello“),因此它们使用最接近的共享祖先(可能是电子邮件表单组件)来保存收件人数据。
如果事件和道具需要经过许多“中间人”组件,LCA可能会很烦人。
要了解更多细节,我建议同事们参考this excellent blog post。(忽略它的示例使用Ember的事实,它的概念适用于许多框架)。

数据容器模式(例如Vuex)

对于父-子通信涉及太多中间人的复杂情况,请使用Vuex或等效的数据容器技术。
当单个存储变得过于复杂或混乱时,请使用命名空间模块。例如,为具有许多相互连接的复杂组件集合(如复杂日历)创建单独的命名空间可能是合理的。

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

如果事件总线(即publish/subscribe)模式对您的应用更有意义(从架构的Angular ),或者您需要从现有的Vue.js应用中删除Vue.js的全局事件总线,Vue.js核心团队现在建议使用第三方库,如mitt。(请参阅第1段中引用的RFC)。
其他
这里有一个小的(可能过于简单)LCA解决方案的例子,这个例子是一个名为whack-a-mole的游戏。
在这个游戏中,玩家“击杀”一只鼹鼠,使它隐藏起来,然后另一只鼹鼠在随机地点出现,就会得到积分。要构建这个包含“鼹鼠”组件的应用程序,人们可能会想,“鼹鼠组件N应该在被击杀后告诉鼹鼠组件Y出现”。但Vue.js不鼓励这种组件通信方法,因为Vue.js应用程序(和html)实际上是tree data structures的。
这可能是一件好事。一个大型/复杂的应用程序,其中节点之间相互通信,没有任何集中的管理器,可能很难调试。此外,使用LCA的组件往往表现出低coupling和高reusability
在此示例中,游戏管理器组件将mole可见性作为道具传递给mole子组件。当“击”(单击)可见的mole时,它将发出一个事件。游戏管理器组件(公共ancestor)接收该事件并修改其状态。Vue.js自动更新道具,因此所有mole组件都将接收新的可见性数据。
第一个

rxztt3cl

rxztt3cl6#

如果我想“破解”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的模式。

l3zydbqr

l3zydbqr7#

在我的例子中,我有一个带有可编辑单元格的表格。当用户从一个单元格单击到另一个单元格编辑内容时,我只希望一次有一个单元格是可编辑的。解决方案是使用父子(道具)与亲子(事件)。在下面的示例中,我将在“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>

相关问题