• 日常搜索
  • 百度一下
  • Google
  • 在线工具
  • 搜转载

Vue.js组件之间通信的设计模式

作为开发人员,我们希望生成可管理和可维护的代码,这也更易于调试和测试。为了实现这一点,我们采用了称为模式的最佳实践。模式是经过验证的算法和架构,可帮助我们以高效且可预测的方式完成特定任务。 

在本教程中,我们将了解最常见的 vue.js 组件通信模式,以及我们应该避免的一些陷阱。我们都知道,在现实生活中,没有单一的解决方案可以解决所有问题。同样,在Vue.js应用程序开发中,没有适用于所有编程场景ios的通用模式。每种模式都有自己的优点和缺点,并且适用于特定的用例。 

对于 Vue.js 开发人员来说,最重要的是了解所有最常见的模式,这样我们就可以为给定的项目选择正确的模式。这将导致正确和有效的组件通信。

为什么正确的组件通信很重要?

当我们使用 Vue.js等基于组件的框架构建应用程序时,我们的目标是使应用程序的组件尽可能地隔离。这使它们可重用、可维护和可测试。为了使组件可重用,我们需要将其塑造成更抽象和解耦(或松散耦合)的形式,因此,我们可以将其添加到我们的应用程序或将其删除,而不会破坏应用程序的功能。 

但是,我们无法在应用程序的组件中实现完全隔离和独立。在某些时候,它们需要相互通信:交换一些数据、更改应用程序的状态等。因此,了解如何正确完成这种通信,同时保持应用程序的工作、灵活和可扩展性对我们来说很重要。

Vue.js 组件通信概述 

在 Vue.js 中,组件之间的通信主要有两种类型: 

  1. 直接的亲子沟通,基于严格的亲子关系和亲子关系。 

  2. 跨组件通信,其中一个组件可以与任何其他组件“对话”,而不管它们之间的关系如何。 

在以下部分中,我们将探讨这两种类型以及适当的示例。 

直接的亲子沟通

Vue.js 开箱即用支持的组件通信标准模型是通过 props 和自定义event实现的父子模型在下图中,您可以看到该模型在运行中的外观。

Vue.js组件之间通信的设计模式  第1张

如您所见,父级只能与其直接子级通信,而子级只能与其父级直接通信。在此模型中,不可能进行同级或跨组件通信。 

在接下来的部分中,我们将从上图中获取组件,并在一系列实际示例中实现它们。

亲子沟通

假设我们拥有的组件是游戏的一部分。大多数游戏都会在其界面的某处显示游戏分数。想象一下,我们在Parent Ascore组件中声明了一个变量,我们想在Child A组件中显示它。那么,我们该怎么做呢? 

Vue.js 使用props将数据从父级分派给子级。传递财产需要三个必要的步骤:

  1. 在孩子中注册属性,如下所示: props: ["score"]

  2. 使用子模板中的注册属性,如下所示: <span>Score: {{ score }}</span>

  3. 将属性绑定到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'
})

CodePen 示例

验证道具

为了简洁明了,我使用它们的速记变体注册了这些道具。但在实际开发中,建议对props 进行验证这将确保道具将收到正确类型的值。例如,我们的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",它将显示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();
    }
  }
})

CodePen 示例

现在,当我们更改分数然后按下“重新渲染父级”按钮时,我们可以看到分数从父级返回到其初始值。所以 Vue 说的是实话!

但是请记住,数组和对象影响它们的父对象,因为它们不是复制的,而是通过引用传递的。

因此,当我们需要改变子元素中的道具时,有两种方法可以解决这种重新渲染的副作用。

使用本地数据属性改变道具

第一种方法是将scoreprop 转换为本地数据属性 ( localScore),我们可以在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;
    }
  }
})

CodePen 示例

现在,如果我们再次按下Rerender Parent按钮,在我们更改分数之后,我们会看到这一次分数保持不变。

使用计算属性改变道具

第二种方法是score在计算属性中使用 prop,它将在其中转换为新值:

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

CodePen 示例

在这里,我们创建了一个 computed doubleScore(),它将父级乘以score 2,然后将结果显示在模板中。显然,按下Rerender Parent 按钮不会有任何副作用。

孩子与父母的沟通

现在,让我们看看组件如何以相反的方式进行通信。

我们刚刚看到了如何改变子组件中的道具,但是如果我们需要在多个子组件中使用该道具怎么办?在这种情况下,我们需要从父级中的源改变 prop,因此所有使用该 prop 的组件都将正确更新。为了满足这个需求,Vue 引入了 自定义事件。 

这里的原则是我们将我们想要做的更改通知给父级,父级进行更改,并且该更改通过传递的道具反映。以下是此操作的必要步骤:

  1. 在孩子中,我们发出一个描述我们想要执行的更改的事件,如下所示: this.$emit('updatingScore', 200)

  2. 在父级中,我们为发出的事件注册一个事件监听器,如下所示: @updatingScore="updateScore"

  3. 当事件发出时,分配的方法将更新道具,如下所示: 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
    }
  }
})

CodePen 示例

我们使用内置 $emit() 方法来发出事件。该方法有两个参数。第一个参数是我们要发出的事件,第二个是新值。

.sync修饰符_

Vue 提供了一个 类似的.sync修饰符 ,在某些情况下我们可能希望将其用作快捷方式。在这种情况下,我们$emit()以稍微不同的方式使用该方法。作为事件参数,我们update:score这样写:  this.$emit('update:score', 200)然后,当我们绑定score道具时,我们.sync像这样添加修饰符:  <child-a :score.sync="score"/>Parent 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()
    }
  }
})

CodePen 示例

为什么不使用 this.$parent 和this.$children进行直接的亲子交流?

Vue 提供了两种api方法,可以让我们直接访问父组件和子组件: this.$parent 和 this.$children起初,将它们用作道具和事件的更快、更容易的替代品可能很诱人,但我们不应该这样做。这被认为是一种不好的做法或反模式,因为它在父组件和子组件之间形成了紧密耦合。后者导致不灵活且易于破坏的组件,难以调试和推理。这些 API 方法很少使用,根据经验,我们应该避免使用它们或谨慎使用它们。

双向组件通信

道具和事件是单向的。道具下降,事件上升。但是通过一起使用 props 和 events,我们可以有效地在组件树上下通信,从而实现双向数据绑定。这实际上是v-model 指令在内部执行的操作。 

跨组件通信

随着我们应用程序复杂性的增加,父子通信模式很快变得不方便和不切实际。props-events 系统的问题在于它直接工作,并且与组件树紧密绑定。与原生事件相比,Vue 事件不会冒泡,这就是为什么我们需要重复发出它们直到达到目标。结果,我们的代码变得臃肿,有太多的事件监听器和发射器。因此,在更复杂的应用程序中,我们应该考虑使用跨组件通信模式。 

让我们看一下下图:

Vue.js组件之间通信的设计模式  第2张

如您所见,在这种任意对任意类型的通信中,每个组件都可以从任何其他组件发送和/或接收数据,而无需中间步骤和中间组件。

在接下来的部分中,我们将探讨跨组件通信的最常见实现。

全局事件总线

全局事件总线是一个 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
    }
  }
})

CodePen 示例

以下是创建和使用事件总线的步骤:

  1. 将我们的事件总线声明为一个新的 Vue 实例,如下所示: const eventBus = new Vue ()

  2. 从源组件发出事件,如下所示: eventBus.$emit('updatingScore', 200)

  3. 在目标组件中监听发出的事件,如下所示: 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 构建复杂应用程序的专门教程

现在让我们探索下图:

Vue.js组件之间通信的设计模式  第3张

如您所见,一个 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:`
    <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',
})

CodePen 示例

在商店里,我们有以下物品:

  • 在状态对象中设置的score变量。

  • 一个incrementScore()突变,它将用给定的值增加分数。

  • 一个score()getter,它将score从状态访问变量并将其呈现在组件中。

  • 一个incrementScoreAsync()动作,它将 incrementScore() 在给定的时间段后使用突变来增加分数。 

在 Vue 实例中,我们使用计算属性而不是 props 来通过 getter 获取分值。然后,为了改变分数,我们在Child A 组件中使用了 mutation  store.commit('incrementScore', 100)Parent B组件中,我们使用 action  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',
})

CodePen 示例

通过使用 Grand Parent组件provide中的选项 ,我们使该 变量可供其所有后代使用。他们每个人都可以通过声明属性来访问它 。而且,如您所见,分数显示在所有组件中。 scoreinject: ['score']

注意:依赖注入创建的绑定不是反应式的。因此,如果我们希望提供者组件中所做的更改反映在其后代中,我们必须将一个对象分配给数据属性并在提供的服务中使用该对象。

为什么不 this.$root 用于跨组件通信?

我们不应该使用的原因与之前描述的 for和描述this.$root 的类似 ——它创建了太多的依赖关系。必须避免依赖任何这些方法进行组件通信。this.$parentthis.$children

如何选择正确的图案

所以你已经知道了组件通信的所有常用方法。但是您如何确定哪一个最适合您的场景呢?

选择正确的模式取决于您参与的项目或您要构建的应用程序。这取决于应用程序的复杂性和类型。让我们探索最常见的场景:

  • 简单的应用程序中,道具和事件将是您所需要的。

  • 中端应用程序将需要更灵活的通信方式,例如事件总线和依赖注入。

  • 对于复杂的大型应用程序,您肯定需要 Vuex 作为功能齐全的状态管理系统的强大功能。

最后一件事。您不需要使用任何已探索的模式,因为其他人告诉您这样做。你可以自由选择和使用你想要的任何模式,只要你设法让你的应用程序工作并且易于维护和扩展。

结论

在本教程中,我们学习了最常见的 Vue.js 组件通信模式。我们看到了如何在实践中实施它们,以及如何选择最适合我们项目的合适的。这将确保我们构建的应用程序使用正确类型的组件通信,使其完全工作、可维护、可测试和可扩展。 


文章目录
  • 为什么正确的组件通信很重要?
  • Vue.js 组件通信概述
  • 直接的亲子沟通
  • 亲子沟通
    • 验证道具
    • 更新子道具
      • 使用本地数据属性改变道具
      • 使用计算属性改变道具
  • 孩子与父母的沟通
    • .sync修饰符_
      • 为什么不使用 this.$parent 和this.$children进行直接的亲子交流?
    • 双向组件通信
  • 跨组件通信
    • 全局事件总线
    • Vuex
    • 依赖注入
      • 为什么不 this.$root 用于跨组件通信?
  • 如何选择正确的图案
  • 结论