学习Vue也有一段时间了,在项目中使用Vue也有好几个了,但Vue组件间的状态管理(数据通信)一直是自己的死穴。对于Vue组件间的数据通信,无外呼是父组件向子组件、子组件向父组件、兄弟组件以及嵌套组件之间的数据通信。而且组件之间的通信方式也有很多种。@Gongph的《Vue 父子组件通信的十种方式》一文就详细的介绍了Vue组件,指的是父子组件之间的数据通信就有差不多十种方式。但很多时候我们组件之间的数据通信不仅仅是停留在父子组件之间的数据通信。比如说还有兄弟组件和嵌套组件之间的数据通信。
如果我们抛开嵌套组件之间的数据通信,我们可以用简单的下图来描述Vue组件之间的数据通信:
事实上除了上图方式对数据进行通信之外,还有一些其他的方式,比如父组件获取子组件数据和事件可以通过:
- 通过给子组件绑定
ref
属性来获取子组件实例 - 通过
this.$children
获取子组件实例
对于子组件获取父组件数据和事件,可以通过:
- 通过
props
传递父组件数据和事件,或者通过$emit
和$on
实现事件传递 - 通过
ref
属性,调用子组件方法,传递数据;通过props
传递父组件数据和事件,或者通过$emit
和$on
实现事件传递 - 通过
this.$parent.$data
或者this.$parevent._data
获取父组件数据,通过this.$parent
执行父组件方法
对于兄弟组件之间数据通信和事件传递,可以通过:
- 利用
eventBus
挂载全局事件 - 利用
$parent
进行数据传递,$parent.$children
调用兄弟组件事件
另外,复杂一点的,可以通过Vuex完成Vue组件数据通信。特别是多级嵌套组件间的数据通信。但如果仅仅是数据之间传递,而不做中间处理,使用Vuex有点浪费。不过,自Vue 2.4版本开始提供了另一种方法:
使用
v-bind="$attrs"
将父组件中不被认为props
特性绑定的属性传递给子组件。
通常该方法会配合interiAttrs
一起使用。之所以这样使用是因为两者的出现使得组件之间跨组件的通信在不依赖Vuex和eventBus
的情况下变得简洁,业务清晰。
其实这也就是我们今天要了解的另一个知识点。多级嵌套组件之间,我们如何借助$attrs
和$listeners
来实现数据之间的通信。
业务场景
刚才提到过,我们接下来要聊的是多级嵌套组件之间的数据通信。为了让事情不变得太过于复杂(因为太复杂,对于初学者而言不易于理解和学习)。这里我们就拿三级组件之间的嵌套来举例。比如我们有三个组件ComponentA
、ComponentB
和ComponentC
,而且它们之间的关系是ComponentA > ComponentB > ComponentC
(>
是包含关系),用下图来描述或许更易于明白他们之间的关系:
就三级嵌套的组件而言,他们的关系相对而言要简单一些:
ComponentA
组件是ComponentB
组件的父组件,他们的关系是父子关系ComponentB
组件是ComponentC
组件的父组件,他们的关系也是父子关系ComponentA
组件是ComponentC
组件的祖先组件,他们的关系是祖孙关系
对于这三个组件之间的数据通信,按照我们前面所掌握的知识,估计想到的是:
props
向下,$emit
向上。
也就是说,ComponentA
向ComponentB
可以通过props
的方式向子组件传递,ComponentB
向ComponentA
通过在ComponentB
组件中$emit
向上发送事件,然后在ComponentA
组件中$on
的方式监听发送过来的事件。对于ComponentB
和ComponentC
两组件之间的通信也可以使用类似的方式。但对于ComponentA
组件到ComponentC
组件之间的通信,需要借助ComponentB
组件做为中转站,当ComponentA
组件需要把信息传递给ComponentC
组件时,ComponentB
接受ComponentA
组件的信息,然后利用属性传递给ComponentC
组件。
就此而言,这是一种解决方案,但如果我们嵌套的组件层级过多时将会导致代码繁琐,代码维护也较困难。
除了上述方式可以完成组件之间数据通信外,还有其他的方式,比如借助Vuex的全局状态共享;使用
eventBus
创建Vue的实例实现事件的监听和发布,从而实现组件之间的数据通信。但都过于太浪费,所以我们应该寻找其他更为简易的解决方案,其中文章开始提到的$attrs
以及$listeners
。
简单地说,利用$attrs
实现祖孙组件间的数据传递,$listeners
实现祖孙组件间的事件监听。接下来看看怎么使用这两个特性来完成跨级嵌套组件之间的数据通信。
术语解释
在具体掌握$attrs
和$listeners
是如何完成组件数据通信之前,先来简单地了解一下他们具体是什么?
Vue的官网对$attrs
和$listeners
的描述分别是这样的:
$attrs
的解释
包含了父作用域中不作为
props
被识别 (且获取) 的特性绑定 (class
和style
除外)。当一个组件没有声明任何props
时,这里会包含所有父作用域的绑定 (class
和style
除外),并且可以通过v-bind="$attrs"
传入内部组件 —— 在创建高级别的组件时非常有用。
$listeners
的解释
包含了父作用域中的 (不含
.native
修饰器的)v-on
事件监听器。它可以通过v-on="$listeners"
传入内部组件 —— 在创建更高层次的组件时非常有用。
官方解释的已经非常的清楚了。事实上,你可以把$attrs
和$listeners
比作两个集合,其中$attrs
是一个属性集合,而$listeners
是一个事件集合,两者都是以对象的形式来保存数据。
更简单地说,利用$attrs
实现祖孙组件间的数据传递,$listeners
实现祖孙组件间的事件监听。而且$attrs
继承所有的父组件属性(除props
传递的属性、class
和style
),一般用在子组件的子元素上;$listeners
是一个对象,里面包含了作用在这个组件上的所有监听器,配合v-on
将所有事件监听器指向这个组件的某个特定的子元素(相当于子组件继承父组件的事件)。
为了更易于帮助大家理解这两个属性,我们还是通过一些简单的示例来演示吧。先来看一个简单的示例:
<!-- ChildComponent.vue -->
<template>
<div class="child-component">
<h1>我是一个 {{ professional }}</h1>
</div>
</template>
<script>
export default {
name: 'ChildComponent',
props: {
professional: {
type: String,
default: '码农'
}
},
created () {
console.log(this.$attrs, this.$listeners)
// 调用父组件App.vue中的triggerTwo()方法
this.$listeners.two()
}
}
</script>
<!-- App.vue -->
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png">
<ChildComponent
:professional = "professional"
:name = "name"
@one.native = "triggerOne"
@two = "triggerTwo"
/>
</div>
</template>
<script>
import ChildComponent from './components/ChildComponent.vue'
export default {
name: 'app',
data() {
return {
professional: '屌丝码农',
name:'大漠'
}
},
components: {
ChildComponent
},
methods: {
triggerOne () {
console.log('one')
},
triggerTwo () {
console.log('two')
}
}
}
</script>
示例代码可以在Github的Vue Demos中获取app-vue-communication项目的
step1
分支获取。
从上面的代码中我们可以看出来,在父组件App.vue
中,调用子组件ChildComponent
时有两个属性和两个方法,共别是其中有一个属性是props
声明的(professional
),事件一个是.native
修饰器(监听组件根元素的原生事件)。
这个简单的示例告诉我们可以通过$attrs
和$listeners
进行数据传递,在需要的地方进行调用和处理。比如上面子组件ChildComponent
中通过this.$listeners.two()
访问了父组件App.vue
中的triggerTwo()
方法。当然,我们还可以通过v-on="$listeners"
一级级地往下传递,不管组件嵌套层级有多深。这个后面我们会详细介绍。
另外,上面的示例中,其中有一个属性是props
,比如professional
属性,另外还有一个非props
属性,比如name
。组件编译之后会把非props
属性当成原始属性对待,从而添加到DOM元素(HTML标签上),比如上例中的name
:
这样的结果或许并不是大家所想要的,如果想去掉HTML标签中name
的属性,以至于该属性不暴露出来,我们可以借助inheritAttrs
属性来完成。
inheritAttrs
的默认值true
,继承所有的父组件属性(除props
的特定绑定)作为普通的HTML特性应用在子组件的根元素上,如果你不希望组件的根元素继承特性设置inheritAttrs: false
,但是class
属性会继承。简单的说,**inheritAttrs:true
继承除props
之外的所有属性;inheritAttrs:false
只继承class
属性**。
如果我们在子组件ChildComponent
中添加inheritAttrs: false
,重新编译出来的代码中name
(非props
)属性再不会暴露出来:
多级嵌套组件数据通信
前面花了很长的篇幅解释了$attrs
和$listeners
以及它们是如何在组件中进行数据通信的。回到我们的示例中来,看看文章开头提以的三级嵌套组件之间的数据是如何借助$attrs
和$listeners
实现数据通信。具体代码可以将分支切换到step2
中:
<!-- ComponentC.vue -->
<template>
<div class="component-c">
<h3>组件C中设置的props: {{ name }}</h3>
<p>组件C中的$attrs: {{ $attrs }}</p>
<p>组件C中的$listeners: {{ $listeners }}</p>
</div>
</template>
<script>
export default {
name: 'ComponentC',
props: {
name: {
type: String,
default: '大漠'
}
},
inheritAttrs: false,
mounted () {
this.$emit('test2')
console.log('ComponentC',this.$attrs, this.$listeners)
}
}
</script>
<!-- ComponentB.vue -->
<template>
<div class="component-b">
<h3>组件B中的props: {{ age }}</h3>
<p>组件B中的$attrs: {{ $attrs }}</p>
<p>组件B中的$listeners: {{ $listeners }}</p>
<hr />
<ComponentC v-bind="$attrs" v-on="$listeners" />
</div>
</template>
<script>
import ComponentC from './ComponentC'
export default {
name: 'ComponentB',
props: {
age: {
type: Number,
default: 30
}
},
inheritAttrs: false,
components: {
ComponentC
},
mounted () {
this.$emit('test1')
console.log('ComponentB',this.$attrs, this.$listeners)
}
}
</script>
<!-- ComponentA.vue -->
<template>
<div class="component-a">
<ComponentB :name="name" :age="age" @on-test1="onTest1" @on-test2="onTest2" />
</div>
</template>
<script>
import ComponentB from './ComponentB'
export default {
name: 'ComponentA',
components: {
ComponentB
},
data () {
return {
name: '大漠_w3cplus',
age: 23
}
},
methods: {
onTest1 () {
console.log('test1 runing...')
},
onTest2 () {
console.log('test2 running...')
}
}
}
</script>
<!-- App.vue -->
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png">
<ComponentA />
</div>
</template>
<script>
import ComponentA from './components/ComponentA.vue'
export default {
name: 'app',
components: {
ComponentA
}
}
</script>
这个时候你在页面中将看到的结果如下:
其于上面的基础上,我们来看一个简单的示例(切到分支step3
),一个模态框的数据通信:
<!-- ModalHeader.vue -->
<template>
<div class="modal-header">
<h5 class="modal-title">{{ modalTitle }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" @click="close">
<span aria-hidden="true">×</span>
</button>
</div>
</template>
<script>
export default {
name: 'ModalHeader',
props: {
modalTitle: {
type: String,
default: 'Modal Title'
}
},
inheritAttrs: false,
methods: {
close () {
this.$emit('on-close')
}
},
mounted () {
console.log('ModalHeader',this.$attrs, this.$listeners)
}
}
</script>
<!-- ModalBody.vue -->
<template>
<div class="modal-body">
<slot>{{ modalContent }}</slot>
</div>
</template>
<script>
export default {
name: 'ModalBody',
props: {
modalContent: {
type: String,
default: 'Modal body text goes here.'
}
},
inheritAttrs: false,
mounted () {
console.log('ModalBody',this.$attrs, this.$listeners)
}
}
</script>
<!-- ModalFooter.vue -->
<template>
<div class="modal-footer">
<button class="btn btn-secondary" data-dismiss="modal" @click="close">{{ secondaryButtonContent }}</button>
<button class="btn btn-primary" @click="save">{{ primaryButtonContent }}</button>
</div>
</template>
<script>
export default {
name: 'ModalFooter',
props: {
secondaryButtonContent: {
type: String,
default: 'Close'
},
primaryButtonContent: {
type: String,
default: 'Save'
}
},
inheritAttrs: false,
methods: {
save () {
this.$emit('on-save')
},
close () {
this.$emit('on-close')
}
},
mounted () {
console.log('ModalFooter',this.$attrs, this.$listeners)
}
}
</script>
<!-- Modal.vue -->
<template>
<div class="modal" tabindex="-1" role="dialog" v-if="show">
<div class="modal-dialog" role="document">
<div class="modal-content">
<ModalHeader v-bind="$attrs" v-on="$listeners" />
<ModalBody v-bind="$attrs" v-on="$listeners" />
<ModalFooter v-bind="$attrs" v-on="$listeners" />
</div>
</div>
</div>
</template>
<script>
import ModalHeader from './ModalHeader'
import ModalBody from './ModalBody'
import ModalFooter from './ModalFooter'
export default {
name: 'Modal',
props: {
show: {
type: Boolean,
default: false
}
},
components: {
ModalHeader,
ModalBody,
ModalFooter
},
inheritAttrs: false,
}
</script>
<!-- MaskBackdrop.vue -->
<template>
<div class="modal-backdrop" v-if="show" @click="close">
</div>
</template>
<script>
export default {
name: 'MaskBackdrop',
props: {
show: {
type: Boolean,
default: false
}
},
inheritAttrs: false,
mounted () {
console.log('MaskBackdrop',this.$attrs, this.$listeners)
},
methods: {
close () {
this.$emit('on-close')
}
}
}
</script>
你将看到的效果如下:
在浏览器调试器中,我们可以看以相应$attrs
和$listeners
打印出来的值:
小结
啰嗦了这么多,主要就是阐述了Vue 2.4版本之后的$attrs
和$listeners
是什么以及怎么利用他们来实现组件之间的数据通信。使用这两个特性可以实现跨组件(嵌套)组件之间的数据通信。最后希望这篇文章对大家或多或少有所收获。结合前面的教程,我们可以了解到组件之间数据通信有很多种方式,具体哪种更好应该根据不同的场景来对待,选择最适合的。如果您在这方面有更多的经验或者文章中有不正之处,烦请路过的大神多多拍正。