组件之间的传值通信
# 方式 1: props
/$emit
/$on
# 1.父组件向子组件传值
- 父组件通过 props 给子组件传值
- 子组件通过定义
props.list
接收指定的父组件传过来的值 - 子组件通过
$emit()
方法给父组件回调方法 - 父组件通过
v-on
/@
来监听对应的emit
事件,回调中的参数就是子组件传回来的参数
父组件
<template>
<div class="parent">
<todo :list="list" @clickItem="clickItem"></todo>
</div>
</template>
<!-- 省略JS不写了 -->
2
3
4
5
6
7
子组件:
<template>
<ul>
<li v-for="(item,index) in list" :key="index" @click="$emit('clickItem',item)">
{{item}}
</li>
</ul>
</template>
<script>
export default {
name: 'Todo',
props: {
list: {
default: [],
type: Array
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 2. 兄弟/跨级组件传值
这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,巧妙而轻量地实现了任何组件间的通信,包括父子
、兄弟
、跨级
。
当我们的项目比较大时,可以选择更好的状态管理解决方案 vuex。
2.1 初始化:
首先需要创建事件总线并将其导出,以便其它模块可以使用或者监听它。我们可以通过两种方式来处理。先来看第一种,新创建一个 .js
文件,比如 event-bus.js
// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()
2
3
或者在 main.js 初始化 EventBus
也可以
// main.js
Vue.prototype.$EventBus = new Vue()
2
2.2 在 a 组件发出一个事件
<!-- A.vue -->
<template>
<button @click="sendMsg()">发送消息</button>
</template>
<script>
import { EventBus } from '../event-bus.js'
export default {
methods: {
sendMsg() {
EventBus.$emit('aMsg', '来自A页面的消息')
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2.3 在 b 组件接收 a 发出的事件
<!-- A.vue -->
<template>
<p>{{msg}}</p>
</template>
<script>
import { EventBus } from '../event-bus.js'
export default {
data() {
return {
msg: ''
}
},
mounted() {
EventBus.$on('aMsg', msg => {
// A发送来的消息
this.msg = msg
})
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2.4 小结一下
EventBus 主要用到 2 个方法:
// 发送消息
EventBus.$emit('事件名', '传递的参数/对象')
// 监听接收消息
EventBus.$on('事件名', () => {
// 接收参数的回调函数
})
2
3
4
5
6
7
but!这样会很大的隐患
vue 是单页应用,如果你在某一个页面
刷新了之后
,与之相关的 EventBus会被移除
,这样就导致业务走不下去
。还要就是如果业务有反复操作的页面,EventBus 在监听的时候就会
触发很多次
,也是一个非常大的隐患。这时候我们就需要好好处理 EventBus 在项目中的关系。通常会用到,在 vue 页面销毁时,同时移除 EventBus 事件监听。
2.5 移除监听事件
import { eventBus } from './event-bus.js'
EventBus.$off('aMsg') // 使用 $off 移除对 aMsg的监听
EventBus.$off() // 移除所有事件频道,不需要添加任何参数 。
2
3
4
# 方式 2:vuex
通过 vuex 的方法,触发值修改,其余组件会进行同步更新
# 方式 3:$attrs
/$listeners
末尾有完整的 demo,下面描述的都是代码片段
# $attrs
官方文档:$attrs (opens new window)
- 继承
所有的父组件属性
(除了 prop 传递的属性、class 和 style ),一般用在子组件的子元素上
实际用途
解决方法 1:children 多接收一个 age 属性,继续传递给 grandson
缺点:冗余!明明 children 无须用到 age,这样对组件定义 props 是极其不友好的
解决方法 2:使用 vuex。
缺点:太大材小用了。vuex 内部那么复杂的东西,用来解决跨级数据传递问题,这是对性能的浪费(虽然现在手机/电脑性能都不差)
像这种时候,$attrs
就非常有用
看个栗子:
<!-- parent组件,父组件 -->
<div class="parent">
<children name="Jioho" age="111" sex="男"></children>
</div>
<!-- children组件 -->
<div class="children">
<grandson :name="name" v-bind="$attrs"></grandson>
</div>
<script>
export default {
props: ['name']
}
</script>
<!-- grandson组件 -->
<div class="grandson">
<div>name:{{name}}</div>
<div>age:{{age}}</div>
<div>sex:{{$attrs['sex']}}</div>
</div>
<script>
export default {
props: ['name', 'age']
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
注意上面的代码:
- 最后
grandson
可以拿到 2 个 props。并且 sex 也可以正常输出 - children 组件只接收了 name 属性
1. 先看 children
组件:
定义了一个参数 props:name
那么,childred 里面的$attrs = {age="Jioho",sex="男"}
。
印证了他的定义
继承
所有的父组件属性
(除了 prop 传递的属性、class 和 style ),一般用在子组件的子元素上
如果这时候 children 的 props 多接收一个 sex。即:props:['name','sex']
。那 $attrs
就只有一个age
属性
1.1 小结:
注意字眼:
继承
、父组件传递的值,不用显示定义(props
) 也可以拥有。只是这个存放在了$attr
用一条公式来带过一下:
$attrs = 父组件传递下来的参数 - 子组件定义的 props - class - style
除去 子组件中的props,class,style
剩下的都可以在$attr
找到。在高阶组件中,如果有非常多不确定的参数要继续传递给子组件,就可以用上
$attrs
详细的 demo 代码可以在末尾找到,自己修改一下传递的参数,修改一下 props 便能理解
# $listeners
官方文档:$listeners (opens new window)
它是一个对象,里面包含了作用在这个组件上的所有监听器,
你就可以配合
v-on="$listeners"
将所有的事件监听器指向这个组件的某个特定的子元素。(相当于子组件继承父组件的事件)和
$attrs
很相似,跨级组件的事件通信。
看个栗子:
<!-- parent 组件 -->
<div class="parent">
parent
<children
:name="name"
:age="age"
:sex="sex"
@testClick="testClick"
@testChildClick="testChildClick"
></children>
</div>
<script>
export default {
methods: {
testClick() {
console.log('testClick ...')
},
testChildClick() {
console.log('testChildClick ...')
}
}
}
</script>
<!-- children 组件 -->
<div class="children">
children - {{name}}
<grandson v-bind="$attrs" :name="name" v-on="$listeners"></grandson>
</div>
<script>
export default {
created() {
console.log('children -----------------')
this.$emit('testClick')
console.log('children -----------------')
}
}
</script>
<!-- grandson 组件 -->
<div class="grandson">
<div>name:{{name}}</div>
<div>age:{{age}}</div>
<div>sex:{{$attrs['sex']}}</div>
</div>
<script>
export default {
created() {
console.log('grandson -----------------')
this.$emit('testClick')
this.$emit('testChildClick')
console.log('grandson -----------------')
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
打印结果:
children -----------------
testClick ...
children -----------------
grandson -----------------
testClick ...
testChildClick ...
grandson -----------------
2
3
4
5
6
7
8
注意的点
children 组件并没有定义任何方法,而
grandson
却 2 个方法都可以调用如果 children 定义了方法,那么
chilren 的会执行
。parent 也会执行
!
小结
$listeners
就是用于传递事件的,如果传递的过程中有同名的事件,那么 2 个事件都会被执行
# 题外拓展 inheritAttrs
官方介绍inheritAttrs (opens new window)
大概的作用: 默认为 true 的时候,会在子组件的节点上添加对应的参数名,这可能会影响我们自己定义参数,所以提供了
inheritAttrs
属性 ,让我们自己操作 dom 的时候可以自定义标签的属性。
默认值 true,继承所有的父组件属性(除 props 的特定绑定)作为普通的 HTML 特性应用在子组件的根元素上
如果你不希望组件的根元素继承特性设置
inheritAttrs: false
,但是 class 属性会继承简单的说
inheritAttrs:true
继承除 props 之外的所有属性inheritAttrs:false
只继承 class 属性)- 注意这里是继承,而不是 props 传值,继承和传值是有区别的!
还是一开始 $attrs
的 demo。默认情况下:标签上的属性都来自props
定义的。
- childred 组件的 props:['age',sex']
- grandson 组件的 props:['sex']
- 组件内的
inheritAttrs
默认为true
字不如图。
操作 1:
在 children
组件中添加 inheritAttrs
为 false
可以看到 children 标签的属性被隐藏了。可是 name 还是可以输出出来。class 不受影响
这时候如果有需求需要在 children
标签上放一些自定义的属性(比如想自定义 name 和 age),也不会和 vue 的机制冲突了
# 最后附上 demo
parent 组件
<template>
<div class="parent">
parent
<children
:name="name"
:age="age"
:sex="sex"
@testClick="testClick"
@testChildClick="testChildClick"
></children>
</div>
</template>
<script>
import children from './children'
export default {
name: 'parent',
data() {
return {
name: 'Jioho',
age: 18,
sex: '男'
}
},
components: {
children: children
},
methods: {
testClick() {
console.log('testClick ...')
},
testChildClick() {
console.log('testChildClick ...')
}
}
}
</script>
<style lang="scss" scoped>
.parent {
width: 400px;
height: 400px;
background-color: green;
text-align: center;
color: #ffffff;
font-size: 18px;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
children 组件
<template>
<div class="children">
children - {{name}}
<grandson v-bind="$attrs" :name="name" v-on="$listeners" @testChildClick="testChildClick"></grandson>
</div>
</template>
<script>
import grandson from './grandson'
export default {
name: 'children',
props: ['name'],
inheritAttrs: false,
components: {
grandson
},
created() {
console.log('children -----------------')
this.$emit('testClick')
console.log('children -----------------')
},
methods: {
testChildClick() {
console.log('子组件调用了')
}
}
}
</script>
<style lang="scss" scoped>
.children {
width: 300px;
height: 300px;
background-color: rgb(138, 194, 138);
text-align: center;
color: #ffffff;
font-size: 18px;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
grandson 组件
<template>
<div class="grandson">
<div>name:{{name}}</div>
<div>age:{{age}}</div>
<div>sex:{{$attrs['sex']}}</div>
</div>
</template>
<script>
export default {
name: 'grandson',
props: ['name', 'age'],
created() {
console.log('grandson -----------------')
this.$emit('testClick')
this.$emit('testChildClick')
console.log('grandson -----------------')
}
}
</script>
<style lang="scss" scoped>
.grandson {
width: 200px;
height: 200px;
background-color: blue;
color: #ffffff;
font-size: 18px;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 方式 4:$parent / $children 与 ref
ref
:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例$parent
/$children
:访问父 / 子实例
这两种方法的弊端是,无法在跨级或兄弟间通信。
拿到实例对象后,就可以进行值的获取,或者修改了,这里就不做展开
# 方式 5:provide/inject
Vue2.2.0 新增 API,这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖
,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。
provide 可以在祖先组件中指定我们想要提供给后代组件的数据或方法,而在任何后代组件中,我们都可以使用 inject
来接收 provide
提供的数据或方法。
看个栗子:
// 父级组件提供 'foo'
export default {
data() {
return {
foo: 'bar'
}
},
provide() {
return {
foo: this.foo
}
}
}
// 子组件注入 'foo'
export default {
inject: ['foo']
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
从这几个问题,了解下 provide/inject
# provide 什么时候可以为对象
,什么时候为函数
?
provide 如果是直接传值(无须从当前页面获取 this 里面的值的话)可以为对象,
如果需要用到当前页面的变量,那就需要函数,来获取 this 对象
// 示范1 provide为对象的时候
export default {
provide: {
foo: 'bar' // 这时候是直接赋值,无须this
}
}
// 示范2 provide的值依赖于当前的this
export default {
data() {
return {
foo: 'bar'
}
},
provide() {
return {
foo: this.foo // 这时候依赖于this,就需要为函数,具体的要理解下原型和原型链。和data为啥不能是对象一个道理
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# provide
和 inject
什么时候是单向数据,什么时候可以双向绑定?
在问题 1 中的代码,数据都是单向数据流,如果在子组件中改变了 foo 。他们并不会影响父组件的值。同理,父组件的 foo 修改了,子组件在 inject
接收到的值也不会变化。这是 vue 故意这样设计的
那有没有双向数据绑定的栗子?有的:上述的情况都是因为传进去的值都是非响应式
的值(相当于只是传递了一个字符串)。只要我们传入的值是一个响应式对象
。那他们就可以实现双向数据绑定
// 父组件传入 foo 对象,注意是对象类型而不是单纯的字符串
export default {
data() {
return {
foo: {
value: 'bar'
}
}
},
provide() {
return {
foo: this.foo // 传入一个响应式对象,因为不是字符串,所以会被监听到
}
}
}
// 子组件接收该对象
export default {
inject: ['foo'],
create() {
console.log(this.foo) // {value:'bar'}
// 成功,并且父组件的 thisi.foo.value 也被修改为了new bar
this.foo.value = 'new bar'
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# provide/inject 好处在哪里和缺点
好处
provide/inject 相当于上面说的
$attrs
。可以说更加优雅了一些。甚至无须一层层传递$attrs
provide/inject 也可以做全局状态管理(vuex)但是不建议这么做。上面问题 2 也说到了,只要传入的是响应式对象,那他们都可以双向数据绑定。
- 建议:如果用 provide/inject 充当全局状态管理的话,只推荐用一些不会变化(一次性数据),可是需要贯穿全局的变量去传递。
- 总结了两条条使用 provide/inject 做全局状态管理的原则:
- 多人协作时,做好作用域隔离
- 尽量使用一次性数据作为全局状态
利用 provide/inject 的特性来编写组件库非常有用。可以实现不同组件间的嵌套,而不需要一层层的数据传递。(可以参考下 elementUI 的表单和按钮设计)
缺点
就是刚才说到的全局状态管理的问题。为什么 provide/inject
不能代替 vuex 成为全局数据管理的首选?
Vuex 和 provide/inject 最大的区别在于,Vuex 中的全局状态的每次修改是可以追踪回溯的,而 provide/inject 中变量的修改是无法控制的,换句话说,你不知道是哪个组件修改了这个全局状态
。
试想,如果有多个后代组件同时依赖于一个祖先组件提供的状态,那么只要有一个组件修改了该状态,那么所有组件都会受到影响。这一方面增加了耦合度,另一方面,使得数据变化不可控。如果在多人协作开发中,这将成为一个噩梦
# 总结
组件之间传值通信的方式非常非常多,每种方案各有优缺点,并不能一套方案打天下,主要还是要结合自身的业务逻辑和复杂程度,选择最优的方法