ホームページ > 記事 > ウェブフロントエンド > Vue コンポーネント間の通信方法の詳細な分析
この記事では、vue に関する関連知識を提供し、主に Vue コンポーネント間の通信方法を紹介します。Vue コンポーネント間の通信は常に重要なトピックですが、正式にリリースされた Vuex 状態管理ソリューションでは、この問題を解決できます。コンポーネント間の通信の問題は非常にうまくいきますが、コンポーネント ライブラリ内で Vuex を使用すると比較的重いことがよくあります。一緒に見てみましょう。皆さんのお役に立てれば幸いです。
[関連する推奨事項: javascript ビデオ チュートリアル、vue.js チュートリアル]Vue コンポーネントの
ライブラリ 開発プロセス中、Vue コンポーネント間の通信は常に重要なトピックでした。正式にリリースされた Vuex 状態管理ソリューションはコンポーネント間の通信の問題を非常にうまく解決できますが、コンポーネント ライブラリ内で Vuex を使用すると、多くの場合比較的重いです。この記事のシステム参考として、Vuex を使用しないコンポーネント間のより実用的な通信方法をいくつかリストします。
今日のトピックに入る前に、まず Vue コンポーネント間の通信に関するいくつかのシナリオを要約しましょう。次のシナリオ:
親コンポーネントは prop を通じて子コンポーネントにデータを渡します
、すべてのプロップは、親プロップと子プロップの間で 一方向の下向きバインディングを形成します。親プロップへの更新は子コンポーネントに下向きに流れますが、その逆は起こりません。これにより、子コンポーネントが親コンポーネントの状態を誤って変更して、アプリケーションのデータ フローが理解しにくくなるのを防ぐことができます。 さらに、親コンポーネントが変更されるたびに、子コンポーネント内のすべてのプロパティが最新の値に更新されます。これは、子コンポーネント内の props を変更すべきではないことを意味します。これを行うと、Vue はブラウザーのコンソールに警告を発行します。
親コンポーネント コンポーネント A:<template>
<p>
<component-b title="welcome"></component-b>
</p>
</template>
<script>
import ComponentB from './ComponentB'
export default {
name: 'ComponentA',
components: {
ComponentB
}
}
</script>
<template>
<p>
<p>{{title}}</p>
</p>
</template>
<script>
export default {
name: 'ComponentB',
props: {
title: {
type: String,
}
}
}
</script>
子コンポーネントは、カスタムを通じて親コンポーネントに渡されます。イベント データ
を通じて親コンポーネントにイベントを生成したり、親コンポーネントで v-on
/# を通じてイベントを生成したりできます。 ##@ モニター。
サブコンポーネント コンポーネント A:
<template> <p> <component-b :title="title" @title-change="titleChange"></component-b> </p> </template> <script> import ComponentB from './ComponentB' export default { name: 'ComponentA', components: { ComponentB }, data: { title: 'Click me' }, methods: { titleChange(newTitle) { this.title = newTitle } } } </script>サブコンポーネント コンポーネント B:
<template> <p> <p @click="handleClick">{{title}}</p> </p> </template> <script> export default { name: 'ComponentB', props: { title: { type: String, } }, methods: { handleClick() { this.$emit('title-change', 'New title !') } } } </script>この例は、内部では非常に単純です。サブコンポーネント ComponentB $emit を通じてイベント
title-change をディスパッチし、
@title-change によってバインドされた
titleChange イベントをリッスンします。親コンポーネント ComponentA では、 ComponentB によって ComponentA に渡されるデータは、
titleChange 関数のパラメーターで取得できます。
兄弟コンポーネント間の通信
プレゼンテーション コンポーネント と
コンテナ コンポーネント(コンテナ コンポーネント) の 2 つのカテゴリに分類されます。
表示コンポーネントは、コンポーネントによって使用されるデータがどのように取得されるか、コンポーネント データがどのように変更されるべきかについては考慮しません。必要なのは、このデータでコンポーネント UI がどのように見えるかを知ることだけです。外部コンポーネントは、プレゼンテーション コンポーネントが必要とするデータと、これらのデータを変更するコールバック関数に props を渡します。プレゼンテーション コンポーネントは単なるユーザーです。
State Promotion
の概念を参照して、2 つの兄弟コンポーネントの上にコンテナ コンポーネントに相当し、処理を担当する親コンポーネントを提供します。兄弟コンポーネントは props を渡します パラメーターとコールバック関数を受け取ることは、兄弟コンポーネント間の通信の問題を解決するためにコンポーネントを表示することと同じです。ComponentA (兄弟コンポーネント A):
<template> <p> <p>{{title}}</p> <p @click="changeTitle">click me</p> </p> </template> <script> export default { name: 'ComponentA', props: { title: { type: String }, changeTitle: Function } } </script>ComponentB (兄弟コンポーネント B):
<template> <p> <p>{{title}}</p> <p @click="changeTitle">click me</p> </p> </template> <script> export default { name: 'ComponentB', props: { title: { type: String }, changeTitle: Function } } </script>ComponentC (コンテナ コンポーネント C):
<template> <p> <component-a :title="titleA" :change-title="titleAChange"></component-a> <component-b :title="titleB" :change-title="titleBChange"></component-b> </p> </template> <script> import ComponentA from './ComponentA' import ComponentB from './ComponentB' export default { name: 'ComponentC', components: { ComponentA, ComponentB }, data: { titleA: 'this is title A', titleB: 'this is title B' }, methods: { titleAChange() { this.titleA = 'change title A' }, titleBChange() { this.titleB = 'change title B' } } } </script>ご覧のとおり、上記の「ステータスの昇格」方法は比較的面倒で、特に兄弟コンポーネント間の通信には親コンポーネントの助けが必要です。複雑で、対処するのが非常に面倒です。 世代を分けたコンポーネント間の通信
$attrs
/$listeners
rovide
/inject
$parent
/$children
实现的 dispatch
和 broadcast
Vue 2.4.0 版本新增了 $attrs
和 $listeners
两个方法。先看下官方对 $attrs
的介绍:
包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定(
class
和style
除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class
和style
除外),并且可以通过v-bind="$attrs"
传入内部组件——在创建高级别的组件时非常有用。
看个例子:
组件A(ComponentA):
<template> <component-a name="Lin" age="24" sex="male"></component-a> </template> <script> import ComponentB from '@/components/ComponentB.vue' export default { name: 'App', components: { ComponentA } } </script>
组件B(ComponetB):
<template> <p> I am component B <component-c v-bind="$attrs"></component-c> </p> </template> <script> import ComponentC from '@/components/ComponentC.vue' export default { name: 'ComponentB', inheritAttrs: false, components: { ComponentC } } </script>
组件C(ComponetC):
<template> <p> I am component C </p> </template> <script> export default { name: 'ComponentC', props: { name: { type: String } }, mounted: function() { console.log('$attrs', this.$attrs) } } </script>
这里有三个组件,祖先组件(ComponentA)、父组件(ComponentB)和子组件(ComponentC)。这三个组件构成了一个典型的子孙组件之间的关系。
ComponetA 给 ComponetB 传递了三个属性 name、age 和 sex,ComponentB 通过 v-bind="$attrs"
将这三个属性再透传给 ComponentC, 最后在 ComponentC 中打印 $attrs
的值为:
{age: '24', sex: 'male'}
为什么我们一开始传递了三个属性,最后只打印了两个属性 age 和 sex 呢?因为在 ComponentC 的props 中声明了 name 属性,$attrs
会自动排除掉在 props 中声明的属性,并将其他属性以对象的形式输出。
说白了就是一句话,$attrs
可以获取父组件中绑定的非 Props 属性。
一般在使用的时候会同时和 inheritAttrs
属性配合使用。
如果你不希望组件的根元素继承 attribute,你可以在组件的选项中设置 inheritAttrs: false
。
在 ComponentB 添加了 inheritAttrs=false
属性后,ComponentB 的dom结构中可以看到是不会继承父组件传递过来的属性:
如果不加上 inheritAttrs=false
属性,就会自动继承父组件传递过来的属性:
再看下 $listeners
的定义:
包含了父作用域中的 (不含
.native
修饰器的)v-on
事件监听器。它可以通过v-on="$listeners"
传入内部组件——在创建更高层次的组件时非常有用。
$listeners
也能把父组件中对子组件的事件监听全部拿到,这样我们就能用一个v-on
把这些来自于父组件的事件监听传递到下一级组件。
继续改造 ComponentB 组件:
<template> <p> I am component B <component-c v-bind="$attrs" v-on="$listeners"></component-c> </p> </template> <script> import ComponentC from '@/components/ComponentC.vue' export default { name: 'ComponentB', inheritAttrs: false, components: { ComponentC } } </script>
这里利用 $attrs
和 $listeners
方法,可以将祖先组件(ComponentA) 中的属性和事件透传给孙组件(ComponentC),这样就可以实现隔代组件之间的通信。
provide/inject
是 Vue 2.2.0 版本后新增的方法。
这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。如果你熟悉 React,这与 React 的上下文特性很相似。
先看下简单的用法:
父级组件:
export default { provide: { name: 'Lin' } }
子组件:
export default { inject: ['name'], mounted () { console.log(this.name); // Lin } }
上面的例子可以看到,父组件通过 privide
返回的对象里面的值,在子组件中通过 inject
注入之后可以直接访问到。
但是需要注意的是,provide
和 inject
绑定并不是可响应的,按照官方的说法,这是刻意为之的。
也就是说父组件 provide 里面的name属性值变化了,子组件中 this.name 获取到的值不变。
如果想让 provide 和 inject 变成可响应的,有以下两种方式:
看一下第一种场景:
祖先组件组件(ComponentA):
export default { name: 'ComponentA', provide() { return { app: this } }, data() { return { appInfo: { title: '' } } }, methods: { fetchAppInfo() { this.appInfo = { title: 'Welcome to Vue world'} } } }
我们把整个 ComponentA.vue 的实例 this
对外提供,命名为 app
。接下来,任何组件只要通过 inject
注入 app 的话,都可以直接通过 this.app.xxx
来访问 ComponentA.vue 的 data
、computed
、methods
等内容。
子组件(ComponentB):
<template> <p> {{ title }} <button @click="fetchInfo">获取App信息</button> </p> </template> <script> export default { name: 'ComponentB', inject: ['app'], computed: { title() { return this.app.appInfo.title } }, methods: { fetchInfo() { this.app.fetchAppInfo() } } } </script>
这样,任何子组件,只要通过 inject
注入 app
后,就可以直接访问祖先组件中的数据了,同时也可以调用祖先组件提供的方法修改祖先组件的数据并反应到子组件上。
当点击子组件(ComponentB)的获取App信息按钮,会调用 this.app.fetchAppInfo
方法,也就是访问祖先组件(ComponentA)实例上的 fetchAppInfo 方法,fetchAppInfo 会修改fetchAppInfo的值。同时子组件(ComponentB)中会监听 this.app.appInfo 的变化,并将变化后的title值显示在组件上。
再看一下第二种场景,通过 Vue.observable
方法来实现 provide
和 inject
绑定并可响应。
基于上面的示例,改造祖先组件(ComponentA):
import Vue from 'vue' const state = Vue.observable({ title: '' }); export default { name: 'ComponentA', provide() { return { state } } }
使用 Vue.observable
定义一个可响应的对象 state,并在 provide 中返回这个对象。
改造子组件(ComponentB):
<template> <p> {{ title }} <button @click="fetchInfo">获取App信息</button> </p> </template> <script> export default { name: 'ComponentInject', inject: ['state'], computed: { title() { return this.state.title } }, methods: { fetchInfo() { this.state.title = 'Welcome to Vue world22' } } } </script>
与之前的例子不同的是,这里我们直接修改了 this.state.title 的值,因为 state 被定义成了一个可响应的数据,所以 state.title 的值被修改后,视图上的 title 也会立即响应并更新,从这里看,其实很像 Vuex
的处理方式。
以上两种方式对比可以发现,第二种借助于 Vue.observable
方法实现 provide
和 inject
的可响应更加简单高效,推荐大家使用这种方式。
先了解下 dispatch 和 broadcast 两个概念:
$on
进行监听$on
进行监听在实现 dispatch 和 broadcast 方法之前,先来看一下具体的使用方法。有 ComponentA.vue 和 ComponentB.vue 两个组件,其中 ComponentB 是 ComponentA 的子组件,中间可能跨多级,在 ComponentA 中向 ComponentB 通信:
组件ComponentA:
<template> <button @click="handleClick">派发事件</button> </template> <script> import Emitter from '../mixins/emitter.js'; export default { name: 'ComponentA', mixins: [Emitter], methods: { handleClick () { this.dispatch('ComponentB', 'on-message', 'Hello Vue.js') } } } </script>
组件ComponentB:
export default { name: 'ComponentB', created () { this.$on('on-message', this.showMessage) }, methods: { showMessage (text) { console.log(text) } } }
dispatch 的逻辑写在 emitter.js
中,使用的时候通过 mixins
混入到组件中,这样可以很好的将事件通信逻辑和组件进行解耦。
dispatch 的方法有三个传参,分别是:需要接受事件的组件的名字(全局唯一,用来精确查找组件)、事件名和事件传递的参数。
dispatch 的实现思路非常简单,通过 $parent
获取当前父组件对象,如果组件的name和接受事件的name一致(dispatch方法的第一个参数),在父组件上调用 $emit
发射一个事件,这样就会触发目标组件上 $on
定义的回调函数,如果当前组件的name和接受事件的name不一致,就递归地向上调用此逻辑。
dispath:
export default { methods: { dispatch(componentName, eventName, params) { let parent = this.$parent || this.$root; let name = parent.$options.name; while (parent && (!name || name !== componentName)) { parent = parent.$parent; if (parent) { name = parent.$options.name } } if (parent) { parent.$emit.apply(parent, [eventName].concat(params)); } } } }
broadcast逻辑和dispatch的逻辑差不多,只是一个是通过 $parent
向上查找,一个是通过 $children
向下查找,
export default { methods: { broadcast(componentName, eventName, params) { this.$children.forEach(child => { const name = child.$options.name if (name === componentName) { child.$emit.apply(child, [eventName].concat(params)) } else { broadcast.apply(child, [componentName, eventName].concat([params])) } }) } } }
【相关推荐:javascript视频教程、vue.js教程】
以上がVue コンポーネント間の通信方法の詳細な分析の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。