# 25. 全局事件总线
# 25.1 全局事件总线(GlobalEventBus)
一种可以在任意组件间通信的方式,本质上就是一个对象,它必须满足以下条件
- 所有的组件对象都必须能看见它
- 这个对象必须能够用
$on$emit$off方法去绑定、触发和解绑事件
使用步骤
- 定义全局事件总线
new Vue({
...
beforeCreate() {
Vue.prototype.$bus = this // 安装全局事件总线,$bus 就是当前应用的 vm
},
...
})
1
2
3
4
5
6
7
2
3
4
5
6
7
使用事件总线
接收数据:A组件想接收数据,则在A组件中给
$bus绑定自定义事件,事件的回调留在A组件自身export default { methods(){ demo(data){...} } ... mounted() { this.$bus.$on('xxx',this.demo) } }1
2
3
4
5
6
7
8
9提供数据:
this.$bus.$emit('xxx',data)
最好在
beforeDestory钩子中,用$off()去解绑当前组件所用到的事件
# main.js
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
el:'#app',
render: h => h(App),
beforeCreate() {
Vue.prototype.$bus = this // 安装全局事件总线
}
})
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
# App.vue
<template>
<div class="app">
<School/>
<Student/>
</div>
</template>
<script>
import Student from './components/Student'
import School from './components/School'
export default {
name:'App',
components:{ School, Student }
}
</script>
<style scoped>.app{background-color: gray;padding: 5px;}</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# School.vue
<template>
<div class="school">
<h2>学校名称:{{ name }}</h2>
<h2>学校地址:{{ address }}</h2>
</div>
</template>
<script>
export default {
name: "School",
data() {
return {
name: "尚硅谷",
address: "北京",
};
},
mounted() { //🔴
// console.log('School',this)
this.$bus.$on("hello", (data) => {
console.log("我是School组件,收到了数据", data);
});
},
beforeDestroy() { //🔴
this.$bus.$off("hello");
},
};
</script>
<style scoped>.school {background-color: skyblue;padding: 5px;}</style>
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
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
# Student.vue
<template>
<div class="student">
<h2>学生姓名:{{ name }}</h2>
<h2>学生性别:{{ sex }}</h2>
<button @click="sendStudentName">把学生名给School组件</button> //🔴
</div>
</template>
<script>
export default {
name:'Student',
data() {
return {
name:'张三',
sex:'男'
}
},
methods: { //🔴
sendStudentName(){
this.$bus.$emit('hello', this.name)
}
}
}
</script>
<style scoped>.student{background-color: pink;padding: 5px;margin-top: 30px;}</style>
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
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
# Todo-List 案例之事件总线
# App.vue
<template>
<div>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<MyHeader :addTodo="addTodo"></MyHeader>
<MyList :todos="todos"></MyList>
<MyFooter :clearAllTodo="clearAllTodo" v-if="todos.length > 0" :fullCheck="fullCheck" :todos="todos"></MyFooter>
</div>
</div>
</div>
</div>
</template>
<script>
import MyHeader from './components/MyHeader'
import MyFooter from './components/MyFooter'
import MyList from './components/MyList'
export default {
name: 'App',
watch: {
todos: {
deep: true,
handler(value) {
localStorage.setItem('todos', JSON.stringify(value))
}
}
},
data() {
return {
// 由于 todos 是 MyHeader组件和MyFooter组件都在使用 所以放在 App 中 (状态提升)
todos: JSON.parse(localStorage.getItem('todos')) || []
}
},
components: {
MyHeader,
MyFooter,
MyList
},
mounted() {
//🔴
this.$bus.$on('checkTodo', this.checkTodo)
this.$bus.$on('deleteTodo', this.deleteTodo)
},
beforeDestroy() {
//🔴
this.$bus.$off('checkTodo')
this.$bus.$off('deleteTodo')
},
methods: {
// 添加一个 todo
addTodo(todoObj) {
this.todos.unshift(todoObj)
},
// 勾选 or 取消勾选 todo
checkTodo(id) {
// console.log(id)
this.todos.some(todo => {
if (todo.id == id) {
todo.done = !todo.done
console.log(todo)
}
})
},
// 删除一个todo
deleteTodo(id) {
this.todos = this.todos.filter(todo => {
return todo.id !== id
})
},
// 全选 or 取消全选
fullCheck(done) {
this.todos.forEach(todo => {
todo.done = done
})
},
// 清除所有已经完成的todo
clearAllTodo() {
this.todos = this.todos.filter(todo => !todo.done)
}
}
}
</script>
<style lang="less">
/*base*/
body {
background: #fff;
}
.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
font-size: 14px;
line-height: 20px;
text-align: center;
vertical-align: middle;
cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
border-radius: 4px;
}
.btn-danger {
color: #fff;
background-color: #da4f49;
border: 1px solid #bd362f;
}
.btn-danger:hover {
color: #fff;
background-color: #bd362f;
}
.btn:focus {
outline: none;
}
.todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-wrap {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
</style>
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
128
129
130
131
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
128
129
130
131
# main.js
// 引入Vue
import Vue from 'vue'
import App from './App.vue'
// 关闭Vue的生产提示
Vue.config.productionTip = false
// 创建vm
new Vue({
render: h => h(App),
beforeCreate() {
//🔴
Vue.prototype.$bus = this
}
}).$mount('#app')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# MyList.vue
<template>
<div>
<ul class="todo-main">
<MyItem v-for="todoObj in todos" :key="todoObj.id" :todo="todoObj">
</MyItem>
</ul>
</div>
</template>
<script>
import MyItem from './MyItem.vue'
export default {
name: 'MyList',
data() {
return {}
},
props: ['todos'],
components: {
MyItem
},
mounted() {},
methods: {}
}
</script>
<style lang="less" scoped>
/*main*/
.todo-main {
margin-left: 0px;
border: 1px solid #ddd;
border-radius: 2px;
padding: 0px;
}
.todo-empty {
height: 40px;
line-height: 40px;
border: 1px solid #ddd;
border-radius: 2px;
padding-left: 5px;
margin-top: 10px;
}
</style>
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
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
# MyItem.vue
<template>
<div>
<li>
<label>
<input type="checkbox" :checked="todo.done" @change="handle(todo.id)" />
<!-- 如下代码也能实现功能,但是不太推荐,因为有点违反原则,因为修改了props -->
<!-- <input type="checkbox" v-model="todo.done"> -->
<span>{{ todo.title }}</span>
</label>
<button class="btn btn-danger" @click="deleteItem(todo.id)">删除</button>
</li>
</div>
</template>
<script>
export default {
name: 'MyItem',
data() {
return {}
},
// 声明接收 todo 对象
props: ['todo'],
mounted() {},
methods: {
handle(id) {
// 通知 App 组件将对应的数据 取反
// 💖
this.$bus.$emit('checkTodo', id)
},
deleteItem(id) {
if (confirm('确定删除吗')) {
// 通知 App 删除指定 id
// 💖
this.$bus.$emit('deleteTodo', id)
}
}
}
}
</script>
<style lang="less" scoped>
/*item*/
li {
list-style: none;
height: 36px;
line-height: 36px;
padding: 0 5px;
border-bottom: 1px solid #ddd;
}
li label {
float: left;
cursor: pointer;
}
li label li input {
vertical-align: middle;
margin-right: 6px;
position: relative;
top: -1px;
}
li button {
float: right;
display: none;
margin-top: 3px;
}
li:before {
content: initial;
}
li:last-child {
border-bottom: none;
}
li:hover {
background-color: gainsboro;
}
li:hover button {
display: block;
}
</style>
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
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
# MyHeader.vue
<template>
<div>
<div class="todo-header">
<input type="text" placeholder="请输入你的任务名称,按回车键确认" v-model="title" @keyup.enter="add" />
</div>
</div>
</template>
<script>
import { nanoid } from 'nanoid'
export default {
name: 'MyHeader',
data() {
return {
title: ''
}
},
props: ['addTodo'],
mounted() {},
methods: {
add() {
// 校验数据
if (!this.title.trim()) return alert('输入不能为空')
// 将用户的输入包装成一个todo对象
const todoObj = { id: nanoid(), title: this.title.trim(), done: false }
// 通知App组件去添加一个 todo 对象
this.addTodo(todoObj)
// 清空输入
this.title = ''
}
}
}
</script>
<style lang="less" scoped>
/*header*/
.todo-header input {
width: 560px;
height: 28px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 4px;
padding: 4px 7px;
}
.todo-header input:focus {
outline: none;
border-color: rgba(82, 168, 236, 0.8);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
}
</style>
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
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
# MyFooter.vue
<template>
<div>
<div class="todo-footer">
<label>
<input type="checkbox" v-model="isAll" />
</label>
<span>
<span>已完成{{ doneTotal }}</span> / 全部{{ todos.length }}
</span>
<button class="btn btn-danger" @click="clearTodos">清除已完成任务</button>
</div>
</div>
</template>
<script>
export default {
name: 'MyFooter',
computed: {
doneTotal() {
return this.todos.filter(todo => todo.done).length
},
isAll: {
get() {
return this.todos.every(todo => todo.done == true) && this.todos.length > 0
},
set(value) {
this.fullCheck(value)
}
}
},
data() {
return {
isfull: false
}
},
props: ['fullCheck', 'todos', 'clearAllTodo'],
mounted() {},
methods: {
clearTodos() {
if (confirm('确定清空吗')) {
this.clearAllTodo()
}
}
}
}
</script>
<style lang="less" scoped>
/*footer*/
.todo-footer {
height: 40px;
line-height: 40px;
padding-left: 6px;
margin-top: 5px;
}
.todo-footer label {
display: inline-block;
margin-right: 20px;
cursor: pointer;
}
.todo-footer label input {
position: relative;
top: -1px;
vertical-align: middle;
margin-right: 5px;
}
.todo-footer button {
float: right;
margin-top: 5px;
}
</style>
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
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