ChatGPT解决这个技术问题 Extra ChatGPT

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

概述

在 Vue.js 2.x 中,model.sync will be deprecated

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

背景

据我了解 Vue.js 2.x,兄弟通信的首选方法是使用存储或事件总线。

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

还值得一提的是“在组件之间传递数据”通常是一个坏主意,因为最终数据流变得无法跟踪并且很难调试。如果一条数据需要多个组件共享,首选全局存储或 Vuex。

[Link to discussion]

和:

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

因此,Evan suggests 使用 $emit()$on()

关注点

我担心的是:

每个商店和活动都有全局可见性(如果我错了,请纠正我);

每次小交流都新建一个店铺太浪费了;

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

问题

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

$emitv-model 结合以模拟 .sync。我认为您应该采用 Vuex 方式

t
tony19

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

组件 1:

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

组件 2:

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

这比定义添加事件中心并将其附加到任何事件消费者更好。
我是这个解决方案的忠实粉丝,因为我真的不喜欢有范围的事件。但是,我并不是每天都使用 VueJS,所以我很好奇是否有人发现这种方法存在问题。
所有答案的最简单的解决方案
不错,简短且易于实现,也易于理解
如果您只想要直接的兄弟姐妹通信,请使用 $parent 而不是 $root
t
tony19

在 Vue.js 2.0 中,我使用了 eventHub 机制,如 in the documentation 所示。

定义集中式事件中心。 const eventHub = new Vue() // 单个事件中心 // 使用全局 mixin 分发到组件 Vue.mixin({ data: function () { return { eventHub: eventHub } } }) 现在在您的组件中,您可以使用 this 发出事件.eventHub.$emit('update', data) 听你做 this.eventHub.$on('update', data => { // 做你的事 })

更新

请参阅 the answer by alex,它描述了一个更简单的解决方案。


请注意:密切关注 Global Mixins,并尽可能避免使用它们,因为根据此链接 vuejs.org/v2/guide/mixins.html#Global-Mixin,它们甚至可能影响第三方组件。
一个更简单的解决方案是使用 @Alex 描述的 - this.$root.$emit()this.$root.$on()
为了将来参考,请不要用其他人的答案更新您的答案(即使您认为它更好并且您参考了它)。链接到备用答案,或者如果您认为应该接受另一个答案,甚至要求 OP 接受另一个答案 - 但是将他们的答案复制到您自己的答案是不好的形式,并且会阻止用户在应得的地方给予信用,因为他们可能只是简单地支持您的只回答。通过不在您自己的答案中包含该答案,鼓励他们导航到(并因此支持)您引用的答案。
感谢您提供宝贵的反馈@GrayedFox,相应地更新了我的答案。
请注意,Vue 3 将不再支持此解决方案。请参阅 stackoverflow.com/a/60895076/752916
t
tony19

状态范围

在设计 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 5 显然是 Pinia。敬请关注。 (Tweet)

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

现在you ask

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

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

从后端接收的数据,

像主题一样的全局 UI 状态,

任何数据持久层,例如保存到后端或与本地存储接口,

吐司消息或通知,

等等

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

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

为避免在全局状态下处理混乱的所有内容,请参阅 Application structure 建议。

参考和方法:边缘案例

尽管存在 props 和 events,但有时您可能仍需要直接访问 JavaScript 中的子组件。它仅作为直接子操作的逃生舱口 - 您应该避免从模板或计算属性中访问 $refs。

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

$parent:边缘案例

与 $root 类似,$parent 属性可用于从子级访问父级实例。这可能很容易成为使用道具传递数据的懒惰替代方案。在大多数情况下,深入到父级会使您的应用程序更难调试和理解,尤其是当您更改父级中的数据时。稍后查看该组件时,将很难弄清楚该突变来自何处。

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

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

有关事件总线模式的最新信息,请参阅 @AlexMA's answer

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

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

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

// 绑定到自定义“更新”事件的组件。 var Item = { template: `

  • {{text}}
  • `, props: { text: Number },mounted() { this.$root.$on('update', () => { console.log(this.text, '还活着'); }); }, }; // 发出事件的组件 var List = new Vue({ el: '#app', components: { Item }, data: { items: [1, 2, 3, 4] }, updated() { this.$root .$emit('update'); }, 方法: { onRemove() { console.log('slice'); this.items = this.items.slice(0, -1); } } });

    记得在 destroyed 生命周期挂钩中删除侦听器。

    组件类型

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

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

    应用特定容器

    通用/展示组件

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

    应用特定容器

    注意:将新的 Composition API 视为这些容器的替代品。

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

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

    以某种方式作用于事件或存储兄弟组件的可见性

    这是范围界定发生的地方。大多数组件不了解商店,并且该组件应该(主要)使用一个命名空间商店模块,其中有限的一组 gettersactions 与提供的 Vuex binding helpers 一起应用。

    通用/展示组件

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

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

    兄弟姐妹交流

    那么,在这一切之后,我们应该如何在两个兄弟组件之间进行通信呢?

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

    ❌ 混合关注点

    从最坏的情况开始,我们的组件将混合表示和业务逻辑。

    // MyInput.vue


    P
    Peter Mortensen

    如果我想“破解”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 之类的东西。


    我已经在使用 vuex,但是我是否应该为每个次要通信创建 vuex 的存储?
    对于这么多信息,我很难说,但我想说,如果你已经在使用 vuex 是的,那就去吧。用它。
    实际上,我不同意我们需要为每个次要通信使用 vuex ......
    不,当然不是,这完全取决于上下文。实际上我的答案远离 vuex。另一方面,我发现你使用 vuex 和中央状态对象的概念越多,我就越不依赖对象之间的通信。但是,是的,同意,这一切都取决于。
    我想说的是,从长远来看,使用 Vuex 是一种更有序、更有弹性的策略。不要将 Vuex 用于受限于一个组件的任何状态,将 Vuex 用于在组件之间泄漏的任何状态,除非有明显的理由不这样做(例如,如果父组件和子组件之间的双向绑定就足够了)。也不要为每一件事都创建一个新的 vuex 模块,你可以在商店中有一个“根”模块来适应应用程序级别的状态。
    o
    omarjebari

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