这是我参与「第四届青训营 」笔记创作活动的第1天。
概述
在基于Vue开发项目时,必然需要用到组件间的通信。本文将总结Vue中常见的组件通信方式,并对每种方式的适用场景进行分析。
组件间通信方式可以分为以下四种类型:
- 父子组件之间通信
- 兄弟组件之间通信
- 祖先与后代之间通信
- 非关系组件之间通信
组件间的通信方式
1.通过props传递
适用场景:父组件给子组件传递信息 父组件在子组件的组件标签中通过字面量传递,子组件通过props接收
通过props传递数据的方式很简单,在此不做示例。
但是在使用props时有一些需要注意的点:
- 不能在子组件中修改props传过来的数据,这样会破坏vue提倡的单向数据流。
- 对于普通类型,在修改数据时会报错,如果有修改需求的话,可以通过data或computed将传过来的数据做一个转换。
- 对于引用类型,修改元素而不修改引用时,不会报错,且在子组件中修改了,父组件中也会做相应的更新,相当于实现了父子组件数据的双向绑定,但是不推荐这么做。如果有父子组件双向数据绑定的需求的话,可以使用v-model或.sync(在后面会介绍)。
2.自定义事件
适用场景:子组件给父组件传递信息 子组件通过$emit触发自定义事件,第二个参数可以传递数据 父组件通过绑定监听器获得传过来的数据
示例:
子组件
this.$emit('add', good)
父组件
<Children @add="cartAdd($event)" />
3.ref
适用场景:父组件需要主动获取子组件中的数据或方法 ref特性可以为子组件赋予一个ID引用,通过这个ID,父组件可以获取子组件的组件实例对象,也就能拿到对象身上的数据和方法了。
父组件
<Children ref="foo" />
this.$refs.foo // 获取子组件实例,通过子组件实例我们就能拿到对应的数据
在使用ref时的注意点: 1.$refs不是响应式的,它只能获得那一时刻的状态,因此要避免在computed中使用获取到的数据。
4.全局事件总线$Bus
适用场景:任意组件之间传递
在main.js中:
// 创建全局事件总线
Vue.prototype.Bus = new Vue();
```
在一个组件中传递数据:
this.$bus.$emit('foo',参数)
在另一个组件中接收:
this.$bus.$on('foo', callback)
5.Vuex数据仓库
适用场景:复杂关系的组件间通信 Vuex相当于一个存储数据的仓库,组件取数据/修改数据等,都对这个仓库进行处理。
6.插槽
适用场景:父组件向子组件传递结构 插槽可以分为
- 默认插槽
- 具名插槽
- 作用域插槽:子组件的数据来源于父组件,但是子组件的自己的结构由父亲决定。
- 默认插槽:
在子组件<submit-button>中:
<button type="submit">
<slot></slot>
</button>
如果我们希望这个 <button> 内绝大多数情况下都渲染文本“Submit”,但是有时候却希望渲染文本为别的东西,怎么实现呢? 我们可以将“Submit”作为后备内容,将它放在 <slot> 标签内:
<button type="submit">
<slot>Submit</slot>
</button>
父级组件中使用 <submit-button> 并且不提供任何插槽内容时:
<submit-button></submit-button>
后备内容“Submit”将会被渲染:
<button type="submit">
Submit
</button>
但是如果我们在父组件中提供内容:
<submit-button>
Save
</submit-button>
则这个提供的内容将会被渲染从而取代后备内容:
<button type="submit">
Save
</button>
2.具名插槽
有时我们写了一个子组件,希望:
<template>
<div class="container">
<header>
<!-- 我们希望把页头放这里 -->
</header>
<main>
<!-- 我们希望把主要内容放这里 -->
</main>
<footer>
<!-- 我们希望把页脚放这里 -->
</footer>
</div>
</template>
对于这样的情况,<slot> 元素有一个特殊的 attribute:name。这个 attribute 可以用来定义额外的插槽:
<template>
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
一个不带 name 的 <slot> 出口会带有隐含的名字“default”。 父组件在向具名插槽提供内容的时候,我们可以在一个 <template> 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称:
<template>
<myslot>
<div>大家好我是父组件</div>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template v-slot:footer>
<p>Here's footer info</p>
</template>
</myslot>
</template>
渲染结果为:
Here might be a page title
大家好我是父组件
A paragraph for the main content.
And another one.
Here's footer info
注意:v-slot只能添加在<template>上,具名插槽v-slot在书写时,可以缩写为#。
3.作用域插槽
作用域插槽比较难理解,大概意思是子组件的数据来源于父组件,但是子组件的自己的结构有父亲决定。 作用域插槽具体理解后续补充。
7.v-model指令
适用场景:实现父子组件的双向数据绑定 v-model的本质是v-bind和v-on的语法糖。在一个组件上使用v-model相当于为这个组件绑定了名为value的prop和名为input的事件。 当组件中的某个prop需要实现和父组件的双向绑定,也就是当子组件中prop值改变时,父组件同时改变,可以使用v-model来实现:
父组件
<template>
<div>
<!--父组件通过v-model将名为value的total传给子组件,同时监听一个名为input的事件-->
<child v-model="total"></child>
</div>
</template>
<script>
import Child from "./child.vue"
export default {
components: {
Child
},
data: function () {
return {
total: 0
};
},
}
</script>
子组件
<template>
<div>
<span>{{value}}</span>
<button @click="reduce">减少5</button>
</div>
</template>
<script>
export default {
props: {
//得到父组件传过来的value
value: Number
},
methods: {
reduce: function(){
//触发名为input的事件,并传递参数,这个参数会赋值给父组件中的total
this.$emit("input", this.value - 5)
}
}
}
</script>
点击子组件中减少5这个按钮,子组件中的value-5,同时父组件中的total-5。
8. .sync修饰符
适用场景:父子组件间数据双向绑定
和v-medol一样,.sync也是一个语法糖。
注意点:v-model和.sync都能实现父子组件数据的双向绑定。相比之下,.sync更灵活,v-model在一个组件中只能有一个,而.sync可以有多个。
9. $parent和$children
适用场景:父子组件通信
$parent 属性可以用来从一个子组件访问父组件的实例,$children 属性可以获取当前实例的直接子组件。
看起来使用 $parent 比使用prop传值更加简单灵活,可以随时获取父组件的数据或方法,又不像使用 prop 那样需要提前定义好。但使用 $parent 会导致父组件数据变更后,很难去定位这个变更是从哪里发起的,所以在绝大多数情况下,不推荐使用。
10. $attrs和$listeners
适用场景:父组件给子组件传递信息
$attrs是组件实例的属性,可以获得父组件传递的props(前提子组件没有通过props接受)
$listeners也是组件实例的属性,可以获取父组件传递过来的自定义事件(以对象形式呈现)