作为开发人员,我们希望生成可管理和可维护的代码,这也更易于调试和测试。为了实现这一点,我们采用了称为模式的最佳实践。模式是经过验证的算法和架构,可以帮助我们以高效且可预测的方式完成特定任务。
在本教程中,我们将了解最常见的 Vue.js 组件通信模式,以及我们应该避免的一些陷阱。我们都知道,在现实生活中,没有单一的解决方案可以解决所有问题。同样,在 Vue.js 应用程序开发中,不存在适用于所有编程场景的通用模式。每种模式都有其自身的优点和缺点,并且适合特定的用例。
对于 Vue.js 开发人员来说,最重要的是了解所有最常见的模式,这样我们就可以为给定的项目选择正确的模式。这将导致正确且高效的组件通信。
当我们使用 Vue.js 等基于组件的框架构建应用时,我们的目标是使应用的组件尽可能隔离。这使得它们可重用、可维护和可测试。为了使组件可重用,我们需要以更抽象和解耦(或松散耦合)的形式塑造它,因此,我们可以将其添加到我们的应用程序中或将其删除,而不会破坏应用程序的功能。
但是,我们无法在应用程序的组件中实现完全隔离和独立。在某些时候,它们需要相互通信:交换一些数据、更改应用程序的状态等。因此,对于我们来说,学习如何正确完成这种通信,同时保持应用程序正常运行、灵活和可扩展非常重要。
在 Vue.js 中,组件之间的通信主要有两种类型:
在以下部分中,我们将探讨这两种类型以及适当的示例。
Vue.js 开箱即用支持的组件通信的标准模型是通过 props 和自定义事件实现的父子模型。在下图中,您可以直观地看到该模型的实际效果。
如您所见,父级只能与其直接子级通信,子级也只能直接与其父级通信。在此模型中,不可能进行同级或跨组件通信。
在下面的部分中,我们将采用上图中的组件,并在一系列实际示例中实现它们。
假设我们拥有的组件是游戏的一部分。大多数游戏都会在界面中的某个位置显示游戏得分。想象一下,我们在Parent A组件中声明了一个score
变量,并且我们希望在Child A组件中显示它。那么,我们怎样才能做到这一点呢?
为了将数据从父级发送到子级,Vue.js 使用 props。传递属性需要三个必要步骤:
props: ["score"]
分数:{{ Score }}
score
变量(在父级模板中),如下所示:
变量(在父级模板中),如下所示:
让我们探索一个完整的示例,以更好地了解实际发生的情况:
// HTML part// JavaScript part Vue.component('ChildB',{ template:`Child B
data {{ this.$data }}登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制
`, }) Vue.component('ChildA',{ template:`Child A
data {{ this.$data }}登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制
Score: {{ score }}// 2.Using`, props: ["score"] // 1.Registering }) Vue.component('ParentB',{ template:``, }) Vue.component('ParentA',{ template:`Parent B
data {{ this.$data }}登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制
Parent A
data {{ this.$data }}登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制
// 3.Binding `, data() { return { score: 100 } } }) Vue.component('GrandParent',{ template:` `, }) new Vue ({ el: '#app' })Grand Parent
data {{ this.$data }}登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制
CodePen 示例
验证道具
为了简洁和清楚起见,我使用它们的速记变体来注册道具。但在实际开发中,建议对 props 进行验证。这将确保道具将收到正确类型的值。例如,我们的
score
属性可以这样验证:props: { // Simple type validation score: Number, // or Complex type validation score: { type: Number, default: 100, required: true } }登录后复制使用 props 时,请确保您了解它们的文字变体和动态变体之间的区别。当我们将 prop 绑定到变量时,它是动态的(例如,
v-bind:score="score"
或其简写:score="score"
) ,因此,prop 的值将根据变量的值而变化。如果我们只是输入一个没有绑定的值,那么该值将按字面意思解释,并且结果将是静态的。在我们的例子中,如果我们编写score="score"
,它将显示score而不是100。这是一个字面意义上的道具。您应该小心这种细微的差异。更新子属性
到目前为止,我们已经成功显示了游戏得分,但在某些时候我们需要更新它。让我们试试这个。
Vue.component('ChildA',{ template:`Child A
data {{ this.$data }}登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制
Score: {{ score }}`, props: ["score"], methods: { changeScore() { this.score = 200; } } })我们创建了一个
changeScore()
方法,该方法应该在我们按下更改分数按钮后更新分数。当我们这样做时,似乎分数已正确更新,但我们在控制台中收到以下 Vue 警告:[Vue warn]:避免直接改变 prop,因为只要父组件重新渲染,该值就会被覆盖。相反,根据 prop 的值使用数据或计算属性。正在变异的道具:“score”正如你所看到的,Vue 告诉我们,如果父级重新渲染,该 prop 将被覆盖。让我们通过使用内置
$forceUpdate()
方法模拟此类行为来测试这一点:Vue.component('ParentA',{ template:`Parent A
data {{ this.$data }}登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制
`, data() { return { score: 100 } }, methods: { reRender() { this.$forceUpdate(); } } }) CodePen 示例
现在,当我们更改分数,然后按重新渲染父级按钮时,我们可以看到分数从父级返回到其初始值。所以 Vue 说的是实话!
但请记住,数组和对象将影响它们的父对象,因为它们不是被复制,而是通过引用传递。
因此,当我们需要改变子级中的 prop 时,有两种方法可以解决这种重新渲染的副作用。
使用本地数据属性改变 Prop
第一种方法是将
score
属性转换为本地数据属性 (localScore
),我们可以在changeScore( )
方法和模板中:Vue.component('ChildA',{ template:`Child A
data {{ this.$data }}登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制
Score: {{ localScore }}`, props: ["score"], data() { return { localScore: this.score } }, methods: { changeScore() { this.localScore = 200; } } })CodePen 示例
现在,如果我们在更改分数后再次按渲染父项按钮,我们会看到这次分数保持不变。
使用计算属性改变 Prop
第二种方法是在计算属性中使用
score
属性,它将被转换为新值:Vue.component('ChildA',{ template:`Child A
data {{ this.$data }}登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制
Score: {{ doubleScore }}`, props: ["score"], computed: { doubleScore() { return this.score * 2 } } })CodePen 示例
在这里,我们创建了一个计算的
doubleScore()
,它将父级的score
乘以 2,然后将结果显示在模板中。显然,按渲染父级按钮不会产生任何副作用。孩子与家长的沟通
现在,让我们看看组件如何以相反的方式进行通信。
我们刚刚了解了如何改变子组件中的某个 prop,但是如果我们需要在多个子组件中使用该 prop 该怎么办?在这种情况下,我们需要从父级中的源中改变 prop,这样所有使用该 prop 的组件都将被正确更新。为了满足这一要求,Vue 引入了自定义事件。
这里的原则是,我们通知父级我们想要做的更改,父级执行该更改,并且该更改通过传递的 prop 反映。以下是此操作的必要步骤:
- 在子进程中,我们发出一个事件来描述我们想要执行的更改,如下所示:
this.$emit('updatingScore', 200)
- 在父级中,我们为发出的事件注册一个事件监听器,如下所示:
@updatingScore="updateScore"
- 当事件发出时,分配的方法将更新属性,如下所示:
this.score = newValue
让我们探索一个完整的示例,以更好地理解这是如何发生的:
Vue.component('ChildA',{ template:`Child A
data {{ this.$data }}登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制
Score: {{ score }}`, props: ["score"], methods: { changeScore() { this.$emit('updatingScore', 200) // 1. Emitting } } }) ... Vue.component('ParentA',{ template:`Parent A
data {{ this.$data }}登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制
// 2.Registering `, data() { return { score: 100 } }, methods: { reRender() { this.$forceUpdate() }, updateScore(newValue) { this.score = newValue // 3.Updating } } }) CodePen 示例
我们使用内置的
$emit()
方法来发出事件。该方法有两个参数。第一个参数是我们要发出的事件,第二个参数是新值。
.sync
修饰符Vue 提供了
.sync
修饰符,其工作原理类似,在某些情况下我们可能希望将其用作快捷方式。在这种情况下,我们以稍微不同的方式使用$emit()
方法。作为事件参数,我们将update:score
如下所示:this.$emit('update:score', 200)
。然后,当我们绑定score
属性时,我们添加.sync
修饰符,如下所示:.在Parent A组件中,我们删除了
updateScore()
方法和事件注册 (@updatingScore="updateScore"
),因为它们不再需要了。Vue.component('ChildA',{ template:`Child A
data {{ this.$data }}登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制
Score: {{ score }}`, props: ["score"], methods: { changeScore() { this.$emit('update:score', 200) } } }) ... Vue.component('ParentA',{ template:`Parent A
data {{ this.$data }}登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制
`, data() { return { score: 100 } }, methods: { reRender() { this.$forceUpdate() } } }) CodePen 示例
为什么不使用
this.$parent
和this.$children
进行直接父子通信?Vue 提供了两种 API 方法,使我们可以直接访问父组件和子组件:
this.$parent
和this.$children
。一开始,可能很想将它们用作道具和事件的更快、更容易的替代品,但我们不应该这样做。这被认为是一种不好的做法或反模式,因为它在父组件和子组件之间形成了紧密耦合。后者会导致组件不灵活且易于损坏,难以调试和推理。这些 API 方法很少使用,根据经验,我们应该避免或谨慎使用它们。双向组件通信
道具和事件是单向的。道具下降,事件上升。但是通过一起使用 props 和 events,我们可以在组件树上有效地进行上下通信,从而实现双向数据绑定。这实际上是
v-model
指令在内部执行的操作。跨组件通信
随着我们的应用程序复杂性的增加,亲子沟通模式很快就会变得不方便且不切实际。 props-events 系统的问题在于它直接工作,并且与组件树紧密绑定。与原生事件相比,Vue 事件不会冒泡,这就是为什么我们需要重复发出它们直到达到目标。结果,我们的代码因过多的事件侦听器和发射器而变得臃肿。因此,在更复杂的应用程序中,我们应该考虑使用跨组件通信模式。
我们看一下下图:
如您所见,在这种任意类型的通信中,每个组件都可以发送和/或接收数据来自任何其他组件,无需中间步骤和中间组件。
在以下部分中,我们将探讨跨组件通信的最常见实现。
全局事件总线
全局事件总线是一个 Vue 实例,我们用它来发出和监听事件。让我们在实践中看看它。
const eventBus = new Vue () // 1.Declaring ... Vue.component('ChildA',{ template:`Child A
data {{ this.$data }}登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制
Score: {{ score }}`, props: ["score"], methods: { changeScore() { eventBus.$emit('updatingScore', 200) // 2.Emitting } } }) ... Vue.component('ParentA',{ template:`Parent A
data {{ this.$data }}登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制
`, data() { return { score: 100 } }, created () { eventBus.$on('updatingScore', this.updateScore) // 3.Listening }, methods: { reRender() { this.$forceUpdate() }, updateScore(newValue) { this.score = newValue } } }) CodePen 示例
以下是创建和使用事件总线的步骤:
- 将我们的事件总线声明为一个新的 Vue 实例,如下所示:
const eventBus = new Vue ()
- 从源组件发出事件,如下所示:
eventBus.$emit('updatingScore', 200)
- 监听目标组件中发出的事件,如下所示:
eventBus.$on('updatingScore', this.updateScore)
在上面的代码示例中,我们从子级中删除
@updatingScore="updateScore"
,并使用created()
生命周期挂钩来监听对于updatingScore
事件。当事件发出时,将执行updateScore()
方法。我们还可以将更新方法作为匿名函数传递:created () { eventBus.$on('updatingScore', newValue => {this.score = newValue}) }登录后复制全局事件总线模式可以在一定程度上解决事件膨胀问题,但它会带来其他问题。可以从应用程序的任何部分更改应用程序的数据,而不会留下痕迹。这使得应用程序更难调试和测试。
对于更复杂的应用程序,事情可能很快就会失控,我们应该考虑专用的状态管理模式,例如 Vuex,它将为我们提供更细粒度的控制、更好的代码结构和组织以及有用的更改跟踪和调试功能。
Vuex
Vuex 是一个状态管理库,专为构建复杂且可扩展的 Vue.js 应用程序而定制。使用 Vuex 编写的代码更加冗长,但从长远来看这是值得的。它对应用程序中的所有组件使用集中存储,使我们的应用程序更有组织、透明且易于跟踪和调试。商店是完全响应式的,因此我们所做的更改会立即反映出来。
在这里,我将向您简要解释什么是 Vuex,并提供一个上下文示例。如果您想更深入地了解 Vuex,我建议您看一下我关于使用 Vuex 构建复杂应用程序的专门教程。
现在让我们研究一下下面的图表:
如您所见,Vuex 应用程序由四个不同的部分组成:
- 状态是我们保存应用程序数据的位置。
- Getter是访问存储状态并将其呈现给组件的方法。
- 突变是实际且唯一允许改变状态的方法。
- 操作是执行异步代码和触发突变的方法。
让我们创建一个简单的商店,看看这一切是如何运作的。
const store = new Vuex.Store({ state: { score: 100 }, mutations: { incrementScore (state, payload) { state.score += payload } }, getters: { score (state){ return state.score } }, actions: { incrementScoreAsync: ({commit}, payload) => { setTimeout(() => { commit('incrementScore', 100) }, payload) } } }) Vue.component('ChildB',{ template:`Child B
data {{ this.$data }}登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制登录后复制
`, }) Vue.component('ChildA',{ template:`Child A