開発者として、私たちは管理しやすく保守しやすく、デバッグやテストも容易なコードを生成したいと考えています。これを実現するために、パターンと呼ばれるベスト プラクティスを採用します。パターンは、特定のタスクを効率的かつ予測可能な方法で達成するのに役立つ実証済みのアルゴリズムとアーキテクチャです。
このチュートリアルでは、最も一般的な Vue.js コンポーネントの通信パターンと、避けるべきいくつかの落とし穴について見ていきます。実生活では、すべての問題に対する唯一の解決策がないことは誰もが知っています。同様に、Vue.js アプリケーション開発にも、すべてのプログラミング シナリオに適用できる普遍的なパターンはありません。各モードには独自の長所と短所があり、特定の使用例に適しています。
Vue.js 開発者にとって最も重要なことは、最も一般的なパターンをすべて理解し、特定のプロジェクトに適切なパターンを選択できるようにすることです。これにより、正確かつ効率的なコンポーネント通信が実現します。
Vue.js などのコンポーネントベースのフレームワークを使用してアプリケーションを構築する場合、目標は、アプリケーションのコンポーネントを可能な限り分離した状態に保つことです。これにより、再利用可能、保守可能、テスト可能になります。コンポーネントを再利用可能にするには、コンポーネントをより抽象的で分離された (または疎結合された) 形式に整形する必要があります。これにより、アプリケーションの機能を損なうことなく、コンポーネントをアプリケーションに追加したり、削除したりできるようになります。
ただし、アプリケーションのコンポーネント間で完全な分離と独立性を達成することはできません。ある時点で、データを交換したり、アプリケーションの状態を変更したりするなど、相互に通信する必要があります。したがって、アプリケーションの機能性、柔軟性、スケーラビリティを維持しながら、この通信を正しく行う方法を学ぶことが重要です。
Vue.js では、コンポーネント間の通信には主に 2 つのタイプがあります:
次のセクションでは、適切な例とともに両方のタイプについて説明します。
Vue.js がそのままサポートするコンポーネント通信の標準モデルは、props とカスタム イベントを通じて実装される親子モデルです。下の画像では、このモデルが動作している様子を視覚的に確認できます。
ご覧のとおり、親は直接の子とのみ通信でき、子は親とのみ直接通信できます。このモデルでは、ピア通信またはコンポーネント間の通信はできません。
次のセクションでは、上の図のコンポーネントを取り上げ、一連の実践的な例で実装します。
私たちが持っているコンポーネントがゲームの一部だとしましょう。ほとんどのゲームでは、インターフェースのどこかにゲームスコアが表示されます。 Parent A コンポーネントで score
変数を宣言し、それを Child A コンポーネントに表示したいとします。では、どうすればよいでしょうか?
親から子にデータを送信するために、Vue.js は props を使用します。属性を渡すには、次の 3 つの必要な手順が必要です。
スコア: {{ スコア }}<span></span>
変数 (親テンプレート内) に次のようにバインドします:
<child-a :score="score"></child-a>
リーリー
コードペンの例検証の小道具
score プロパティは次のように検証できます:
リーリー
v-bind:score="score" またはその省略形
:score="score")、 prop の値は変数の値に基づいて変化します。バインドせずに値を入力した場合、値は文字通り解釈され、結果は静的になります。この例では、
score="score" と記述すると、
100 ではなく score が表示されます。これは文字通りの小道具です。この微妙な違いには注意する必要があります。
Vue.component('ChildA',{ template:` <div id="child-a"> <h2>Child A</h2> <div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">data {{ this.$data }}
我们创建了一个 changeScore()
方法,该方法应该在我们按下更改分数按钮后更新分数。当我们这样做时,似乎分数已正确更新,但我们在控制台中收到以下 Vue 警告:
[Vue warn]:避免直接改变 prop,因为只要父组件重新渲染,该值就会被覆盖。相反,根据 prop 的值使用数据或计算属性。正在变异的道具:“score”
正如你所看到的,Vue 告诉我们,如果父级重新渲染,该 prop 将被覆盖。让我们通过使用内置 $forceUpdate()
方法模拟此类行为来测试这一点:
Vue.component('ParentA',{ template:` <div id="parent-a"> <h2>Parent A</h2> <div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">data {{ this.$data }}
CodePen 示例
现在,当我们更改分数,然后按重新渲染父级按钮时,我们可以看到分数从父级返回到其初始值。所以 Vue 说的是实话!
但请记住,数组和对象将影响它们的父对象,因为它们不是被复制,而是通过引用传递。
因此,当我们需要改变子级中的 prop 时,有两种方法可以解决这种重新渲染的副作用。
第一种方法是将 score
属性转换为本地数据属性 (localScore
),我们可以在 changeScore( )
方法和模板中:
Vue.component('ChildA',{ template:` <div id="child-a"> <h2>Child A</h2> <div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">data {{ this.$data }}
CodePen 示例
现在,如果我们在更改分数后再次按渲染父项按钮,我们会看到这次分数保持不变。
第二种方法是在计算属性中使用 score
属性,它将被转换为新值:
Vue.component('ChildA',{ template:` <div id="child-a"> <h2>Child A</h2> <div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">data {{ this.$data }}
CodePen 示例
在这里,我们创建了一个计算的 doubleScore()
,它将父级的 score
乘以 2,然后将结果显示在模板中。显然,按渲染父级按钮不会产生任何副作用。
现在,让我们看看组件如何以相反的方式进行通信。
我们刚刚了解了如何改变子组件中的某个 prop,但是如果我们需要在多个子组件中使用该 prop 该怎么办?在这种情况下,我们需要从父级中的源中改变 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> <div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">data {{ this.$data }}
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> <div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">data {{ this.$data }}
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:` <div id="child-a"> <h2>Child A</h2> <div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">data {{ this.$data }}
CodePen 示例
以下是创建和使用事件总线的步骤:
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> <div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">data {{ this.$data }}
data {{ this.$data }}
data {{ this.$data }}
CodePen 示例
在商店里,我们有以下产品:
score
变量。incrementScore()
突变,它将按给定值增加分数。score()
getter,它将从状态访问 score
变量并将其呈现在组件中。incrementScoreAsync()
操作,该操作将使用 incrementScore()
突变在给定时间段后增加分数。 在 Vue 实例中,我们不使用 props,而是使用计算属性通过 getter 来获取分数值。然后,为了更改分数,在 Child A 组件中,我们使用突变 store.commit('incrementScore', 100)
。在Parent B组件中,我们使用操作 store.dispatch('incrementScoreAsync', 3000)
。
在结束之前,让我们探讨另一种模式。它的用例主要用于共享组件库和插件,但为了完整性值得一提。
依赖注入允许我们通过 provide
属性定义服务,该属性应该是一个对象或返回对象的函数,并使其可供组件的所有后代使用,而不仅仅是其组件直接孩子。然后,我们可以通过 inject
属性使用该服务。
让我们看看实际效果:
Vue.component('ChildB',{ template:` <div id="child-b"> <h2>Child B</h2> <div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">data {{ this.$data }}
data {{ this.$data }}
data {{ this.$data }}
CodePen 示例
通过使用祖父组件中的 provide
选项,我们将 score
变量设置为可供其所有后代使用。他们每个人都可以通过声明 inject: ['score']
属性来访问它。而且,正如您所看到的,分数显示在所有组件中。
注意:依赖注入创建的绑定不是反应性的。因此,如果我们希望提供程序组件中所做的更改反映在其后代中,我们必须将一个对象分配给数据属性并在提供的服务中使用该对象。
this.$root
进行跨组件通信?我们不应该使用 this.$root
的原因与之前描述的 this.$parent
和 this.$children
的原因类似——它创建了太多的依赖关系。必须避免依赖任何这些方法进行组件通信。
所以你已经了解了组件通信的所有常用方法。但您如何决定哪一个最适合您的场景呢?
选择正确的模式取决于您参与的项目或您想要构建的应用程序。这取决于您的应用程序的复杂性和类型。让我们探讨一下最常见的场景:
最后一件事。您不必仅仅因为别人告诉您这样做就需要使用任何已探索的模式。您可以自由选择和使用您想要的任何模式,只要您设法保持应用程序正常运行并且易于维护和扩展。
在本教程中,我们学习了最常见的 Vue.js 组件通信模式。我们了解了如何在实践中实施它们以及如何选择最适合我们项目的正确方案。这将确保我们构建的应用程序使用正确类型的组件通信,使其完全正常工作、可维护、可测试和可扩展。
以上がVue.js コンポーネント間の通信のデザイン パターンの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。