一、组件间通信的几种方式
props/$emit$parent/$childrenprovide/injectrefeventBus中央事件总线v-slotv-model.sync修饰符Vuex$attrs/$listenerslocalStorage/sessionStorage
二、组件间通信的分类
- 父子间通信
| 传值方式 | 方法 |
|---|---|
| 父传子 | props、$parent、provide/inject、v-slot、v-model、.sync 修饰符、$attrs/$listeners |
| 子传父 | $emit、$children |
| 父子互传 | ref、eventBus 中央事件总线、Vuex、localStorage/sessionStorage |
- 兄弟间通信
| 传值方式 | 方法 |
|---|---|
| 兄弟互传 | eventBus 中央事件总线 、 Vuex 、 localStorage/sessionStorage |
- 跨组件通信
| 传值方式 | 方法 |
|---|---|
| 兄弟互传 | provide/inject 、 eventBus 中央事件总线 、 Vuex 、 $attrs/$listeners 、 localStorage/sessionStorage |
三、通信方式详解
1、props/$emit
- 父组件通过
props向子组件传递数据 - 子组件通过
$emit向父组件发送数据
父组件
<template>
<son :data="sonData" @changeData="changeText"/>
</template>
<script>
import son from './son'
export default {
name: 'parent',
components: {son},
data () {
return {
sonData: 'son data'
}
},
methods: {
changeText (txt) {
this.sonData = txt
}
}
}
</script>
子组件 son.vue
<template>
<div class="box">
<div class="txt">{{data}}</div>
<button @click="toChange">click</button>
</div>
</template>
<script>
export default {
name: 'son',
props: ['data'],
methods: {
toChange () {
this.$emit('changeData', 'son data change')
}
}
}
</script>
注意:
props只允许父级向子级传递,不允许跨级传递;只读不可修改,所有的修改都会失效和报错$emit接收两个参数(arg1, arg2), arg1为传递数据的事件,arg2为需要传递的数据- 父组件 需要在引用的子组件上添加 ‘@传递数据的事件’ 属性来获取
$emit发送的数据
2、$parent/$children
- 子组件通过
$parent直接访问父组件的实例 - 父组件通过
$children访问所有的子组件实例,并且可以递归向上或向下无限访问,直到根实例或者最内层组件
父组件
<template>
<div class="box">
<son />
<button @click="toClick">click</button>
</div>
</template>
<script>
import son from './son'
export default {
name: 'ParentChildren',
components: {son},
data () {
return {
parent: '$parent/$children parent'
}
},
methods: {
toClick () {
this.$children[0].changeSon()
},
changeParent () {
this.parent += ' change'
}
}
}
</script>
子组件 son.vue
<template>
<div class="box">
<div class="text">{{son}}</div>
<div class="text">{{parent}}</div>
<button @click="toChange">change</button>
</div>
</template>
<script>
export default {
name: 'ParentChildrenSon',
data() {
return {
son: '$parent/$children son'
}
},
computed: {
parent() {
return this.$parent.parent
}
},
methods: {
toChange () {
this.$parent.changeParent()
},
changeSon () {
this.son += ' change'
}
}
}
</script>
注意:
- 虽然Vue允许递归向上或者向下无限访问,但实际操作中不建议这么做,因为这种操作会使父子组件紧耦合,而且会使父组件的状态因为可能被任意组件修改而难以理解
$children并不保证顺序,也不是响应式
3、provide/inject
- 允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效
- 解决了跨域组件间的通讯问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系
祖先组件
<template>
<parent />
</template>
<script>
import parent from './parent'
export default {
name: 'ProvideInjectRoot',
components: {parent},
data () {
return {
content: 'provide inject root'
}
},
provide () {
return {
text: this.content
}
}
}
</script>
父组件 parent.vue
<template>
<div class="box">
<div class="root">root: {{text}}</div>
<div class="parent">parent: {{textCur}}</div>
<son class="son" />
</div>
</template>
<script>
import son from './son'
export default {
name: 'ProvideInjectParent',
components: {son},
inject: ['text'],
provide () {
return {
parent: this.textCur
}
},
computed: {
textCur() {
return `${this.text}'s child`
}
}
}
</script>
子组件 son.vue
<template>
<div class="box">
<div class="root">root: {{text}}</div>
<div class="parent">parent: {{parent}}</div>
</div>
</template>
<script>
export default {
name: 'ProvideInjectSon',
inject: ['text', 'parent']
}
</script>
注意:
provide和inject绑定并不是可响应的
provide与inject实现数据响应式的三种方式
provide传递祖先组件的实例,然后在子孙组件中注入依赖,这样就可以在子孙组件中直接修改祖先组件实例的属性。缺点是实例上挂载了很多不需要的属性。
// 祖先组件
...
provide () {
return {
text: this
}
}
// 子孙组件
<template>
<div>{{text.content}}</div>
</template>
<script>
export default {
...
inject: ['text']
}
</script>
- 使用
Vue.observable优化响应式provide
// 祖先组件
<template>
<div>
<parent />
<button @click="toChange">change</button>
</div>
</template>
<script>
import parent from './parent'
import Vue from 'vue'
export default {
...
provide () {
this.text = Vue.observable({val: this.content})
return {
text: this.text
}
},
methods: {
toChange() {
this.text.val = 'provide inject change'
}
}
}
</script>
// 父组件
<template>
<div>root: {{text.val}}</div>
</template>
<script>
export default {
...
inject: ['text']
}
</script>
provide里返回一个函数,获取组件的动态数据
// 祖先组件
<template>
<div>
<parent />
<button @click="toChange">change</button>
</div>
</template>
<script>
import parent from './parent'
export default {
...
provide () {
return {
newText: () => this.content
}
},
methods: {
toChange() {
this.content = 'provide inject change'
}
}
}
</script>
// 父组件
<template>
<div>nText: {{nText}}</div>
</template>
<script>
export default {
...
inject: ['newText'],
computed: {
nText () {
return this.newText()
}
}
}
</script>
4、ref
- 如果用在普通的 DOM 元素上,引用指向的就是 DOM 元素
- 如果用在子组件上,引用就指向组件实例,可以通过实例直接调用组件的方法或访问数据
父组件
<template>
<div class="box">
<son ref="son" />
<button @click="toClick">click</button>
</div>
</template>
<script>
import son from './son'
export default {
name: 'RefParent',
components: {son},
methods: {
toClick() {
const $son = this.$refs.son
console.log($son.text) // ref son
$son.toChange()
console.log($son.text) // ref son change
}
}
}
</script>
子组件 son.vue
<template>
<div class="txt">{{text}}</div>
</template>
<script>
export default {
name: 'RefSon',
data() {
return {
text: 'ref son'
}
},
methods: {
toChange () {
this.text = 'ref son change'
}
}
}
</script>
5、eventBus 中央事件总线
通过创建一个空的Vue实例作为中央事件总线(事件中心),用$emit/$on来触发事件和监听事件,实现父子、兄弟、跨级组件间的通信。
// main.js
Vue.prototype.$bus = new Vue()
父组件
<template>
<div>
<div class="root">{{message}}</div>
<son-a />
<son-b />
</div>
</template>
<script>
import sonA from './son_a'
import sonB from './son_b'
export default {
name: 'EventBus',
components: {sonA, sonB},
data() {
return {
message: ''
}
},
mounted() {
this.$bus.$on('change', data => this.message = data)
}
}
</script>
子组件A son_a.vue
<template>
<button @click="changeMessage">change</button>
</template>
<script>
export default {
name: 'EventBusSonA',
methods: {
changeMessage() {
this.$bus.$emit('change', 'message is change')
}
}
}
</script>
子组件B son_b.vue
<template>
<div class="box">
<div class="son">{{message}}</div>
<grandson />
</div>
</template>
<script>
import grandson from './grandson'
export default {
name: 'EventBusSonB',
components: {grandson},
data() {
return {
message: ''
}
},
mounted() {
this.$bus.$on('change', data => this.message = data)
}
}
</script>
孙组件 grandson.vue
<template>
<div class="grandson">{{message}}</div>
</template>
<script>
export default {
name: 'EventBusGrandson',
data() {
return {
message: ''
}
},
mounted() {
this.$bus.$on('change', data => this.message = data)
}
}
</script>
注意: 当项目较大,使用
eventBus容易造成难以维护的灾难
6、v-slot
- 父组件中通过在引用的子组件标签内添加
template标签,并用v-slot提供具名插槽,将父组件中的值传递给子组件。 - 子组件中通过同名插槽接收值。
父组件
<template>
<son>
<template v-slot:child>{{message}}</template>
</son>
</template>
<script>
import son from './son'
export default {
name: 'VSlot',
components: {son},
data() {
return {
message: 'v-slot'
}
}
}
</script>
子组件 son.vue
<template>
<div class="son">
<slot name="child"></slot>
</div>
</template>
注意:
v-slot在template标签中用于提供具名插槽或需要接收prop的插槽,如果不指定 v-slot ,则取默认值 default。
7、v-model
v-model作用于表单input、textarea、select元素上,可创建双向数据绑定。v-model本质上是语法糖,负责监听用户的输入事件来更新数据,并在某种极端场景下进行一些特殊处理。
v-model双向绑定原理:
a、v-bind绑定value属性的值;
b、v-on绑定input事件到监听函数中,监听函数获取最新的值并赋值到绑定的属性中。
<input v-model="inputValue" />
// 等价于
<input :value="inputValue" @input="inputValue = $event.target.value" />
v-model进行组件通信的原理:
a、 父组件通过v-model向子组件传递值,子组件通过props接收value字段,将它展示到子组件自己的input上。
b、 当子组件自身发生改变时,触发自身的input方法,然后触发父组件的事件方法,改变父组件的value,进而改变接收的props,实现自身展示的改变。
即子组件值改变时,触发表单元素事件,并通过$emit向父组件传递事件和值。
父组件
<template>
<div>
<son v-model="inputValue" />
{{inputValue}}
</div>
</template>
<script>
import son from './son'
export default {
name: 'VModel',
components: {son},
data() {
return {
inputValue: 'v-model'
}
}
}
</script>
子组件 son.vue
<template>
<div>
<input type="text" :value="value" @input="$emit('input', $event.target.value)" />
{{value}}
</div>
</template>
<script>
export default {
name: 'VModelSon',
model: {
prop: 'value',
event: 'input'
},
props: ['value']
}
</script>
注意:
- 子组件中
model属性的prop值与表单元素的值有关,如为单选框、复选框时,prop值为checked- 子组件中
model属性的event值为子组件要更新父组件值时需要注册的方法
8、.sync修饰符
- 在父组件中通过添加
.sync修饰符绑定一个属性名,并将值传递给子组件 - 在子组件中使用
props接收父组件传递的值,并通过this.$emit('update:属性名', value)的形式向父组件传递值
父组件
<template>
<div>
<son :message.sync="mess" />
{{mess}}
</div>
</template>
<script>
import son from './son'
export default {
name: 'Sync',
components: {son},
data() {
return {
mess: 'sync'
}
}
}
</script>
子组件 son.vue
<template>
<button @click="$emit('update:message', 'sync had change')">change</button>
</template>
<script>
export default {
name: 'SyncSon',
props: ['mess']
}
</script>
9、Vuex
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
初始化Vuex模块
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const state = {
testA: 'default a',
testB: 'default b'
}
const mutations = {
receiveTestA(state, payload) {
state.testA = payload.testA
},
receiveTestB(state, payload) {
state.testB = payload.testB
}
}
export default new Vuex.Store({
state,
mutations
})
父组件
<template>
<div>
<son-a />
<son-b />
</div>
</template>
<script>
import sonA from './son_a'
import sonB from './son_b'
export default {
name: 'Vuex',
components: {sonA, sonB}
}
</script>
子组件A son_a.vue
<template>
<div>
<p>testA:{{testA}}</p>
<p>testB:{{testB}}</p>
<button @click="ChangeA">changeA</button>
</div>
</template>
<script>
import $store from './store'
export default {
name: 'VuexSonA',
computed: {
testA() {
return $store.state.testA
},
testB() {
return $store.state.testB
}
},
methods: {
ChangeA () {
$store.commit('receiveTestA', {
testA: 'testA has change'
})
}
}
}
</script>
子组件B son_b.vue
<template>
<div>
<p>testA:{{testA}}</p>
<p>testB:{{testB}}</p>
<button @click="ChangeB">changeB</button>
</div>
</template>
<script>
import $store from './store'
export default {
name: 'VuexSonB',
computed: {
testA() {
return $store.state.testA
},
testB() {
return $store.state.testB
}
},
methods: {
ChangeB () {
$store.commit('receiveTestB', {
testB: 'testB has change'
})
},
}
}
</script>
10、$attrs/$listeners
用在父组件传递数据给子组件或者孙组件
$attrs
- 继承所有的父组件属性(除了
prop传递的属性、class和style) - 当一个组件没有声明任何
prop时,这里会包含所有父作用域的绑定 (class和style除外 ),并且可以通过v-bind="$attrs"传入内部组件。通常配合inheritAttrs选项一起使用。
$listeners
- 是一个对象,包含了父作用域中的v-on事件监听器,可以配合
v-on="$listeners"将所有的事件监听器指向这个组件的某个特定的子元素
父组件 parent.vue
<template>
<div>
<son :message="mess" @test="receive" />
</div>
</template>
<script>
import son from './son'
export default {
name: 'AttrsListeners',
components: {son},
data() {
return {
mess: 'parent'
}
},
methods: {
receive(data) {
this.mess = data
}
}
}
</script>
子组件 son.vue
<template>
<div>
{{$attrs.message}}
<grandson v-bind="$attrs" v-on="$listeners" />
</div>
</template>
<script>
import grandson from './grandson'
export default {
name: 'AttrsListenersSon',
components: {grandson}
}
</script>
孙组件 grandson.vue
<template>
<div>
<p>{{$attrs.message}}</p>
<button @click="Change">change</button>
</div>
</template>
<script>
export default {
name: 'AttrsListenersGrandson',
methods: {
Change () {
this.$emit('test', 'grandson change parent')
// 上级组件中v-on绑定了$listeners,此处可跨级触发parent组件中的test事件
}
}
}
</script>
如遇到表单事件时,下级组件触发上级组件事件可省略,如下:
<!-- 父组件 parent.vue -->
<template>
<div>
<p>parent's mess: {{mess}}</p>
<input type="text" v-model="mess">
<son :message="mess" @keyup="receive" />
</div>
</template>
<script>
import son from './son'
export default {
name: 'AttrsListeners',
components: {son},
data() {
return {
mess: 'parent'
}
},
methods: {
receive(e) {
this.mess = e.target.value
}
}
}
</script>
<!-- 子组件 son.vue -->
<template>
<div>
<p>son's mess: {{$attrs.message}}</p>
<input type="text" v-model="message" v-on="$listeners" />
<grandson v-bind="$attrs" v-on="$listeners" />
</div>
</template>
<script>
import grandson from './grandson'
export default {
name: 'AttrsListenersSon',
components: {grandson},
data() {
return {
message: 'son'
}
}
}
</script>
<!-- 孙组件 grandson.vue -->
<template>
<div>
<p>grandson's mess: {{$attrs.message}}</p>
<input type="text" v-model="message" v-on="$listeners" />
</div>
</template>
<script>
export default {
name: 'AttrsListenersGrandson',
data() {
return {
message: 'grandson'
}
}
}
</script>
11、localStorage/sessionStorage
localStorage和sessionStorage用法相同,在Vue组件通信过程中,当值改变时,需要通过事件触发获取并更新,无法同步更新。因此不推荐使用。