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