萝三画室

详解vue-从熟悉到熟练-part4-组件间通讯

在组件化的世界观中,一个页面是由多个组件组装起来的。那么这些组件是如何协同工作的呢?本节就要介绍,在vue的context之下,组件之间是如何通讯的。

基本通讯方法:父子组件通讯,父向子传递数据,子向父提交事件

首先需要澄清一点:vue实际上也是单向数据流,v-model所提供的双向数据流不过是一个语法糖(后面有栗子)。那么在vue中,数据流动是单向的,只能从父向子传递数据,并且父组件传递过来的数据,子组件无权直接修改。那么根据前几节的内容,相信你已经知道:
父组件通过props向子组件传递数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//子组件 Text.js
<template>
<div>{{text}}</div>
</template>
<script>
export default {
props: ['text'],
}
</script>
//父组件
<template>
<Text text="hello world"/>
</template>
<script>
import Text from './Text.js'
export default {
components: {
Text,
},
}
</script>

子组件通过emit向父组件提交事件,并在父组件中监听事件

1
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
//子组件 Text.js
<template>
<div @click=handleClickText>{{text}}</div>
</template>
<script>
export default {
props: ['text'],
methods: {
handleClickText() {
this.$emit('clickText')
},
}
}
</script>
//父组件
<template>
<Text text="hello world" @clickText=clickText/>
</template>
<script>
import Text from './Text.js'
export default {
components: {
Text,
},
methods: {
clickText() {
alert('text click');
}
}
}
</script>

我们在子组件被点击时,向父组件提交了自定义的‘clickText’事件,并在父组件中监听事件,用clickText函数来响应事件。如果有需要,我们在子组件中提交事件时,也可以传参。

1
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
//子组件 Text.js
<template>
<div @click=handleClickText>{{text}}</div>
</template>
<script>
export default {
props: ['text'],
methods: {
handleClickText() {
this.$emit('clickText', 'text click')
},
}
}
</script>
//父组件
<template>
<Text text="hello world" @clickText=clickText/>
</template>
<script>
import Text from './Text.js'
export default {
components: {
Text,
},
methods: {
clickText(value) {
alert(value);
}
}
}
</script>

API通讯方法:父子组件通讯,通过API引用

前边讲的父子间通讯方法是vue中最基本的方法,这里我们讲的是vue通过API提供的一些父子组件引用方法
在子组件中通过$parent获得父实例,在父组件中通过$children获得子实例
这种方法尽可能避免使用,因为它是非响应式的,并且会使得逻辑分散到不同地方,容易导致混乱的状况。

1
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
//子组件 Text.js
<template>
<div @click=handleClickText>{{text}}</div>
</template>
<script>
export default {
props: ['text'],
methods: {
handleClickText() {
this.$parent.clickText();
},
}
}
</script>
//父组件
<template>
<Text text="hello world"/>
</template>
<script>
import Text from './Text.js'
export default {
components: {
Text,
},
methods: {
clickText() {
alert('text click');
}
}
}
</script>

上面的例子并非这种通讯方法的应用场景,其实使用props和event就可以了。接下来我们讲一下,可能用API通讯的场景:
父组件也是一个自定义组件,并且状态在父组件内部维护
假设我们想要些一个select-option的通用组件,期望它使用起来是这样的:

1
2
3
4
5
6
<template>
<Select>
<Option>选项一</Option>
<Option>选项二</Option>
</Select>
</template>

并且需要的交互式是,点击select,option面板就显示,点击一个option,option面板就关闭,并且选中的值传递到外部。

总结下来,控制option面板的显隐需要一个状态;保存当前选中的值需要一个状态。对于这样的场景,如果我们用props和emit的方式,就需要把两个状态以及对应的事件响应办法全部写到使用这个通用组件的组件内。如果多个组件都用到了这个select-option,那么就要把同样的逻辑写多份。并且,这些逻辑本身是业务无关的,没有必要把它交给使用者处理。因此,我们的想法是,把这些option组件相关的逻辑封到select组件内管理。这样,select-option组件在使用起来,就只需要关心给option什么值,以及选中后的值保存给哪个变量就可以了。
实现:

1
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
//Select
<template>
<div class="select" tabindex="0" @blur="handleHideOptions">
<div class="select-input" :class="{'input-selecting': canSelect}" @click="handleSwitchOptions">
<input :value="selectedValue" :placeholder="placeholder" disabled/>
<label class="select-label" :class="{'single-row': singleRow}">{{selectedObject.label}}</label>
<span class="select-input-icon"></span>
</div>
<ul class="select-options" :style="scrollStyle" v-show="canSelect">
<slot />
</ul>
</div>
</template>
<script>
export default {
props: {
multiple: {
type: Boolean,
default: false,
},
value: [String, Object],
disabled: {
type: Boolean,
default: false,
},
placeholder: String,
isScroll: {
type: Boolean,
default: false,
},
scrollItems: {
type: Number,
default: 5,
},
singleRow: {
type: Boolean,
default: false,
},
},
data() {
return {
selectedValue: this.value,
selectedObject: {},
isShowOptions: false,
};
},
computed: {
scrollStyle() {
return this.isScroll ? { maxHeight: `${(this.scrollItems * 30) + (7 * 2)}px` } : null;
},
hasOptions() {
return this.$slots.default;
},
canSelect() {
return this.hasOptions && this.isShowOptions;
},
},
methods: {
handleSwitchOptions() {
if (this.disabled) return;
this.isShowOptions = !this.isShowOptions;
},
handleHideOptions() {
this.isShowOptions = false;
},
setSelectedObject(value) {
const { multiple, selectedObject } = this;
if (multiple) {
this.selectedObject = { ...selectedObject, value };
} else {
this.selectedObject = value;
}
this.setSelectedValue();
},
setSelectedValue() {
if (this.selectedValue === this.selectedObject.value) return;
this.selectedValue = this.selectedObject.value;
this.$emit('input', this.selectedValue);
},
},
};
</script>
//Option
<template>
<li class="select-options-item" :class="{'selected-item': isSelected}" @click="handleClickOption">
<span class="selected-icon"></span>
<div class="item-content">{{label || value}}</div>
</li>
</template>
<script>
export default {
props: {
value: {
require: true,
},
label: {
default: '',
},
},
methods: {
setSelectedObject(parentValue = this.$parent.selectedValue) {
const { value, label } = this;
if (parentValue === value) {
this.$parent.setSelectedObject({ value, label: label || value });
}
},
handleClickOption() {
const { value, label } = this;
this.$parent.setSelectedObject({ value, label: label || value });
this.$parent.handleHideOptions();
},
},
computed: {
isSelected() {
return _.isEqual(this.$parent.selectedValue, this.value);
},
},
watch: {
'$parent.value': 'setSelectedObject',
},
mounted() {
this.setSelectedObject();
},
};
</script>

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<v-select v-model="targetStatus" style="width: 100px">
<v-option v-for="item in LOG_STATUS" :label="item.content" :value="item.type" :key="item.type"/>
</v-select>
</template>
<script>
export default {
data() {
return {
targetStatus: 'ALL',
LOG_STATUS: {
ALL: { type: 'ALL', content: '全部' },
SUCCESS: { type: 'SUCCESS', content: '成功', color: 'green' },
FAIL: { type: 'FAIL', content: '失败', color: 'red' },
},
}
}
}
</script>

非父子间通讯:小体量用global event bus

前面是父子组件之间的通讯办法,数据可以通过两两父子间通讯不断传递下去。但是当传递的次数变多时,我们就需要在这个过程中的每个组件中都定义props和响应事件。十分繁琐并且难以管理。应对于这种情况,我们可以定义一个全局的组件,在其中保存状态和监听事件,这样在每个组件里都可以使用数据已经提交事件了。这就是global event bus。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//定义event bus
Object.defineProperties(Vue.prototype, {
$bus: {
get() {
return new Vue();
}
}
})
//在某个组件中提交事件
<template>
<div @click="$bus.$emit('someClick')"></div>
</template>
//在某个组件中监听事件
<template>
<div></div>
</template>
<script>
export default {
mounted() {
this.$bus.$on('someClick', () => {
//do something
})
}
}
</script>

非父子间通讯:大体量的终极解决方案,vuex

vuex是vue官方维护的全局状态管理插件,相当于vue的Redux。具体用法这里就不再详述,请查阅官方文档。

以上就是vue的组件间通讯方式。在写业务组件时,通常用props/emit和vuex就可以满足需求。对于一些通用UI组件,可能在一些场景中用AP和event bus通讯方式更合适,请按需使用吧~