概述
在 Vue.js 2.x 中,model.sync
will be deprecated。
那么,在 Vue.js 2.x 中的兄弟组件之间进行通信的正确方法是什么?
背景
据我了解 Vue.js 2.x,兄弟通信的首选方法是使用存储或事件总线。
根据 Evan(Vue.js 的创建者):
还值得一提的是“在组件之间传递数据”通常是一个坏主意,因为最终数据流变得无法跟踪并且很难调试。如果一条数据需要多个组件共享,首选全局存储或 Vuex。
和:
.once 和 .sync 已弃用。道具现在总是单向下降。为了在父作用域中产生副作用,组件需要显式地发出一个事件,而不是依赖于隐式绑定。
因此,Evan suggests 使用 $emit()
和 $on()
。
关注点
我担心的是:
每个商店和活动都有全局可见性(如果我错了,请纠正我);
每次小交流都新建一个店铺太浪费了;
我想要的是对兄弟组件的一些 范围 events
或 stores
可见性。 (或者也许我不明白上面的想法。)
问题
那么,兄弟组件之间的正确通信方式是什么?
$emit
与 v-model
结合以模拟 .sync
。我认为您应该采用 Vuex 方式
您甚至可以缩短它并将 root Vue
实例用作全局事件中心:
组件 1:
this.$root.$emit('eventing', data);
组件 2:
mounted() {
this.$root.$on('eventing', data => {
console.log(data);
});
}
在 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,它描述了一个更简单的解决方案。
this.$root.$emit()
和 this.$root.$on()
状态范围
在设计 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 状态,
任何数据持久层,例如保存到后端或与本地存储接口,
吐司消息或通知,
等等
因此,您的组件可以真正专注于它们本来应该做的事情,管理用户界面,而全局商店可以管理/使用一般业务逻辑并通过 getters 和 actions 提供清晰的 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: `
记得在 destroyed
生命周期挂钩中删除侦听器。
组件类型
免责声明:以下 "containers" versus "presentational" components 只是构建项目的一种方式,现在有多种替代方案,例如可以有效替换“应用特定容器”的新 Composition API我在下面描述。
为了协调所有这些通信,以简化可重用性和测试,我们可以将组件视为两种不同的类型。
应用特定容器
通用/展示组件
同样,这并不意味着应该重用通用组件或不能重用特定于应用程序的容器,但它们有不同的职责。
应用特定容器
注意:将新的 Composition API 视为这些容器的替代品。
这些只是包装其他 Vue 组件(通用或其他应用程序特定容器)的简单 Vue 组件。这是 Vuex 存储通信应该发生的地方,并且该容器应该通过其他更简单的方式进行通信,例如 props 和事件侦听器。
这些容器甚至可以完全没有原生 DOM 元素,让通用组件处理模板和用户交互。
以某种方式作用于事件或存储兄弟组件的可见性
这是范围界定发生的地方。大多数组件不了解商店,并且该组件应该(主要)使用一个命名空间商店模块,其中有限的一组 getters
和 actions
与提供的 Vuex binding helpers 一起应用。
通用/展示组件
这些应该从 props 接收数据,对自己的本地数据进行更改,并发出简单的事件。大多数时候,他们根本不应该知道 Vuex 商店的存在。
它们也可以称为容器,因为它们的唯一职责可能是分派给其他 UI 组件。
兄弟姐妹交流
那么,在这一切之后,我们应该如何在两个兄弟组件之间进行通信呢?
举个例子更容易理解:假设我们有一个输入框,它的数据应该在应用程序之间共享(树中不同位置的兄弟姐妹)并通过后端持久保存。
❌ 混合关注点
从最坏的情况开始,我们的组件将混合表示和业务逻辑。
// MyInput.vue
如果我想“破解”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
是的,那就去吧。用它。
就我而言,我有一个带有可编辑单元格的表格。当用户从一个单击到另一个以编辑内容时,我只希望一次可编辑一个单元格。解决方案是使用父子(道具)和子父(事件)。在下面的示例中,我正在遍历“行”数据集并使用 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>
不定期副业成功案例分享