文章目录
- 一、全局事件总线 (GlobalEventBus)
- 1. 总线前言
- 2. 安装全局事件总线
- 3. 使用总线事件
- 4. 解绑总线事件
- 二、Todo案例应用全局事件总线
- 三、消息订阅与发布
- 1. 前言
- 2. 使用步骤
- 四、Todo案例应用消息订阅
- 五、Todo案例编辑Item
- 六、$nextTick
- 修改后的Todo完整代码
一、全局事件总线 (GlobalEventBus)
1. 总线前言
全局事件总线可以实现任意组件间通信。总线是独立于所有组件之外的。
假设总线名为x,总线x应该满足这两个条件:所有组件都能看的见,且能调用$on(绑定),$off(解绑),$emit(触发)
问题1:绑在谁身上,才能让所有组件都能看的见
可以将总线绑在Vue原型:Vue.prototype
上。之前学过有一个重要的内置关系:VueComponent.prototype.__proto__===Vue.prototype
。有了这个关系,可以将组件实例对象访问到Vue原型上的属性、方法(即顺着图中绿色的线)。
// main.js
Vue.prototype.x = {a:1,b:2}; //随便给总线x赋了个值
在School组件及Student组件中检验
// School组件
mounted () {
console.log('School', this.x);
},
// Student组件
mounted () {
console.log('Student', this.x);
},
确实所有组件都能看到x
问题二:总线得能调用$on/$emit/$off
$on/$emit/$off
都在Vue的原型对象上,x的值是对象时,无法调用这三个方法。所以x的值要么是vm,要么是vc
Vue.prototype.x = vm/vc
- 绑定vc
// Vue.extend({}) 返回的是VueComponent函数,(就是刚学组件时,创建组件的写法)
const Demo = Vue.extend({})
// 通过构造函数得到组件实例,d就是组件实例,即vc
const d = new Demo()
Vue.prototype.x = d;
- 绑定vm
由于main.js中有一个现成的vm,所以一般选择赋值vm:
写在(1)处,vm还为创建;写在(3)处,有点晚了,此时App整个组件已经放到页面上去了,组件里对于$on的调用已经执行了,会报错。正确写法:
2. 安装全局事件总线
一般名称不用x,用$bus
- 方式一:安装vc
const Demo = Vue.extend({})
const d = new Demo()
Vue.prototype.x = d;
- 方式二:安装vm(更通用)
new Vue({
el: '#app',// 挂载容器
render: h => h(App),
beforeCreate () {
// 安装全局事件总线
Vue.prototype.$bus = this
}
})
3. 使用总线事件
School组件与Student组件是兄弟组件。School需要获取到Student组件内的学生姓名(name)的值。
School需要借助总线得到数据,所以School组件得给总线绑定事件:
mounted () {
// 给总线绑定事件‘getName’,并写好事件的回调函数
this.$bus.$on('getName', (name) => {
console.log('School组件收到了学生姓名', name);
})
},
Student组件触发总线上的事件,进而调用了School里的回调函数
methods: {
giveName () {
this.x.$emit('getName', this.name)
}
},
4. 解绑总线事件
组件在总线上绑定事件是想通过总线获取某些数据。所有组件都往这个总线bus上绑定事件,所以当组件被销毁时,该组件在总线$bus上绑定的事件也应该被解绑,不应该再占着了。
School组件
beforeDestroy () {
// 在组件销毁之前,在总线中解绑该事件
this.$bus.$off('getName') // 指明要解绑的事件
// this.$bus.$off() 这样写会将所有人给总线绑定的事件都解绑了
}
二、Todo案例应用全局事件总线
之前App组件与孙组件MyItem通信是通过MyList,以props的方式。现在改为全局事件总线的方式
1、安装全局事件总线
// 创建vm实例
new Vue({
// 将App组件放入容器中
render: h => h(App),
beforeCreate () {
Vue.prototype.$bus = this // 安装事件总线
}
}).$mount('#app') // 挂载容器
2、App.vue
取消给MyList传递函数,MyList也取消接收,取消给MyItem传递函数。MyItem同样也取消props接收函数。
绑定事件
mounted () {
// 事件总线上绑定事件
this.$bus.$on('checkTodo', this.checkTodo)
this.$bus.$on('deleteTodo', this.deleteTodo)
},
beforeDestroy () {
// 组件销毁时,销毁总线上的事件
this.$bus.$off(['checkTodo', 'deleteTodo'])
},
3、MyItem.vue
三、消息订阅与发布
消息订阅与发布是一个理念,不是技术。
1. 前言
以报纸的订阅与发布为例,包含两部分:
1、订阅报纸:需要提供住址
2、邮递员送报纸:报纸
消息的订阅与发布:
1、订阅消息:说明订阅什么消息 以及 回调函数
2、发布消息:发送消息内容
A组件订阅了C组件的demo消息,回调函数是test。C组件一发布demo消息,A由于订阅了该消息,其回调函数test就会被调用,消息内容666就以参数的形式传递给A组件。
需要数据的人----订阅消息
提供数据的人----发布消息。
2. 使用步骤
以School组件需要Student组件的学生姓名数据为例:
-
安装pubsub:
npm i pubsub-js
(用这个库去实现消息订阅与发布的理念) -
引入(School与Student都要引入):
import pubsub from 'pubsub-js'
-
接收数据:School组件想接收数据,则在School组件中订阅消息,订阅的回调留在A组件自身。
mounted () { // 订阅消息(消息名,回调函数(消息名,参数)) // msgName的值是消息名,data才是需要传递的参数 // 将pubId放在vc上,方便后续取消订阅 this.pubId = pubsub.subscribe('hello', (msgName, data) => { console.log('School订阅消息') console.log(data); }) },
-
Student提供数据
giveInfo () { pubsub.publish('hello', this.name) }
-
最好在beforeDestroy钩子中,用
PubSub.unsubscribe(pid)
去取消订阅。beforeDestroy () { // 取消订阅 pubsub.unsubscribe(this.pubId) }
四、Todo案例应用消息订阅
将MyItem里的删除改为消息订阅的形式:
App.vue
import pubsub from 'pubsub-js'
methods:{
// 因为回调函数的第一个参数是msgName,函数里用不到这个参数,
// 所以采用一个_占位,表示接收参数但不用
deleteTodo (_, id) {
this.todos = this.todos.filter((todo) => {
return todo.id !== id
})
},
}
mounted () {
// 订阅消息
this.pubId = pubsub.subscribe('deleteTodo', this.deleteTodo)
},
beforeDestroy () {
// 取消订阅
pubsub.unsubscribe(this.pubId)
},
MyItem.vue
import pubsub from 'pubsub-js'
// 处理删除
handleDelete (id) {
if (confirm('确定要删除吗?')) {
pubsub.publish('deleteTodo', id)
}
}
五、Todo案例编辑Item
1、鼠标移到item上,显示编辑按钮
2、 点击编辑按钮时,前边变成input框,框里的内容是todo.title。
3、编辑完成之后,input框失去焦点时,又变成文字展示。
解决:给todo身上加一个isEdit属性。
1、 鼠标悬浮在MyItem上,显示编辑按钮
<!--MyItem.vue-->
<button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
<button class="btn btn-edit"">编辑</button>
<style>
/* 按钮的样式是写在App.vue里的
因为同一个项目中,不同组件的删除按钮都是同一个风格的,因此只需要写一遍即可。不需要再每个组件里都写一遍。
*/
.btn-edit {
color: #fff;
background-color: skyblue;
border: 1px solid rgb(98, 182, 216);
margin-right: 5px;
}
</style>
2、点击编辑按钮时,前边变成input框,框里的内容是todo.title。
添加点击事件,给todo添加isEdit
属性。如果是这样添加属性:todo.isEdit = true
。这个属性确实加进todo里了,但是,这不是响应式的,点击编辑按钮时,这个属性值确实改变了,但不会被Vue监测到,也不会引起页面的更新。所以应该添加响应式的属性
<!--当处于编辑状态时,不应该有编辑按钮,因此采用v-show-->
<button class="btn btn-edit" @click="handleEdit(todo) v-show="!todo.isEdit"">编辑</button>
<script>
handleEdit (todo) {
// 防止每次点击编辑,都给todo加一下isEdit属性,因此用if判断一下是否需要添加该属性
// if里的条件判断还可以写成:if ('isEdit' in todo)
if (todo.isEdit !== undefined) {
console.log('todo里有isEdit属性了');
todo.isEdit = true
} else {
console.log('todo没有isEdit属性');
this.$set('todo', 'isEdit', true)
}
},
</script>
页面的变化:文字内容与输入框只能存在一个
<span v-show="!todo.isEdit">{{ todo.title }}</span>
<input type="input" :value="todo.title" v-show="todo.isEdit" />
3、编辑完成之后,input框失去焦点时,又变成文字展示。
给输入框添加一个失去焦点事件
<input
type="input"
:value="todo.title"
v-show="todo.isEdit"
@blur="handleBlur(todo,$event)"
/>
<script>
// 这个函数里不用再考虑给todo添加isEdit属性了,既然失去焦点,说明曾经获得过焦点,也就是能够编辑,有isEdit属性
handleBlur (todo, e) {
todo.isEdit = false
// 判断输入是否为空,为空的话不能够进行修改
if (!e.target.value.trim()) return alert('输入为空')
// 触发全局总线的事件,真正修改todo数据
this.$bus.$emit('updateTodo', todo.id, e.target.value)
}
</script>
App.vue
// 编辑todo内容
updateTodo (id, title) {
this.todos.forEach((todo) => {
if (todo.id === id) {
todo.title = title
}
})
},
mounted () {
this.$bus.$on('updateTodo', this.updateTodo)
},
beforeDestroy () {
// 组件销毁时,销毁总线上的事件
this.$bus.$off(['updateTodo'])
},
仍然存在的问题:
点击编辑按钮后,若不想编辑了,需要先点击输入框(因为变成输入框的时候,输入框并没有自动获取焦点),再让数据框失去焦点,才能够让其不是编辑状态。
六、$nextTick
需要:一点击编辑按钮,输入框自动获取焦点。
<input
type="input"
:value="todo.title"
v-show="todo.isEdit"
@blur="handleBlur(todo, $event)"
ref="inputTitle"
/>
<script>
// 处理编辑
handleEdit (todo) {
if (todo.isEdit !== undefined) {
console.log('todo里有isEdit属性了');
todo.isEdit = true
} else {
console.log('todo没有isEdit属性');
this.$set(todo, 'isEdit', true)
}
// 让输入框获取焦点
this.$refs.inputTitle.focus()
},
</script>
这样写发现并不起作用,原因是:
vue会将handleEdit这个回调函数的所有代码都执行完后,再去重新解析模板,而不是执行一条代码,重新解析一次模板。
案例中是用v-show控制input框的出现,执行完19行之前的代码时,isEdit的值发生了变化,v-show的值为true,但是模板并未重新解析,此时input框虽然确实存在,但是还是隐藏状态。所以执行19行代码,对input进行的操作并不起作用 (如果一个input框隐藏了,再调用input框的focus(),input框不会获取焦点)
解决办法一:定时器
// 处理编辑
handleEdit (todo) {
...if...
// 让输入框获取焦点
setTimeout(() => {
this.$refs.inputTitle.focus()
}, 200)
},
解决方法二(官方方法):$nextTick
- 语法:
this.$nextTick(回调函数)
- 作用:在下一次DOM更新结束后执行其指定的回调
- 什么时候用:当改变数据后,要基于更新后的新DOM进行某些操作时,要在
$nextTick
所指定的回调函数中执行。
handleEdit (todo) {
...if...
// 让输入框获取焦点
this.$nextTick(() => {
this.$refs.inputTitle.focus()
})
},
简单来说,$nextTick
所指定的回调,会在DOM节点更新之后再执行。
修改后的Todo完整代码
MyItem.vue
<template>
<li>
<label>
<!-- 添加checked属性使得复选框被勾选上 -->
<input
type="checkbox"
:checked="todo.done"
@change="handleCheck(todo.id)"
/>
<!-- 展示todo内容 -->
<span v-show="!todo.isEdit">{{ todo.title }}</span>
<input
type="input"
:value="todo.title"
v-show="todo.isEdit"
@blur="handleBlur(todo, $event)"
ref="inputTitle"
/>
</label>
<button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
<button class="btn btn-edit" @click="handleEdit(todo)" v-show="!todo.isEdit">编辑</button>
</li>
</template>
<script>
import pubsub from 'pubsub-js'
export default {
name: 'MyItem',
// 接收MyList组件传递的对象
props: ["todo"],
methods: {
// 处理是否勾选
handleCheck (id) {
// this.checkTodo(id)
this.$bus.$emit('checkTodo', id)
},
// 处理删除
handleDelete (id) {
if (confirm('确定要删除吗?')) {
// 通知App,删除对应的todo
// this.deleteTodo(id) props方法
// this.$bus.$emit('deleteTodo', id) 全局事件总线
// 消息订阅
pubsub.publish('deleteTodo', id)
}
},
// 处理编辑
handleEdit (todo) {
// 这个属性确实加进todo里了,但是不是响应式的,这个属性的修改不会被Vue监测到,进而不会引起页面的更新
// todo.isEdit = true
// 防止每次点击编辑,都给todo加一下isEdit属性,因此用if判断一下是否需要添加该属性
// 'isEdit' in todo 也可以这样判断
if (todo.isEdit !== undefined) {
console.log('todo里有isEdit属性了');
todo.isEdit = true
} else {
console.log('todo没有isEdit属性');
this.$set(todo, 'isEdit', true)
}
this.$nextTick(() => {
this.$refs.inputTitle.focus()
})
// 自动获取焦点
this.$nextTick(() => {
this.$refs.inputTitle.focus()
})
},
// 输入框失去焦点回调函数(真正执行修改逻辑)
handleBlur (todo, e) {
todo.isEdit = false
if (!e.target.value.trim()) return alert('输入为空')
this.$bus.$emit('updateTodo', todo.id, e.target.value)
}
}
}
</script>
App.vue
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<!-- 给子组件传函数 -->
<MyHeader @addTodo="addTodo"></MyHeader>
<!-- 给子组件传数据 -->
<MyList :todos="todos"></MyList>
<MyFooter
:todos="todos"
@checkAllTodo="checkAllTodo"
@clearAllTodo="clearAllTodo"
></MyFooter>
</div>
</div>
</div>
</template>
<script>
import pubsub from 'pubsub-js'
import MyHeader from './components/MyHeader'
import MyFooter from './components/MyFooter'
import MyList from './components/MyList'
export default {
name: 'App',
components: {
MyHeader,
MyFooter,
MyList
},
data () {
return {
// 3.读取数据
todos: JSON.parse(localStorage.getItem('todos')) || []
}
},
methods: {
// 添加todoObj
addTodo (todoObj) {
this.todos.unshift(todoObj)
},
// 勾选or取消勾选一个todo
checkTodo (id) {
this.todos.forEach((todo) => {
if (todo.id === id) {
todo.done = !todo.done
}
})
},
// 编辑todo内容
updateTodo (id, title) {
this.todos.forEach((todo) => {
if (todo.id === id) {
todo.title = title
}
})
},
// 删除一个todo
deleteTodo (_, id) {
this.todos = this.todos.filter((todo) => {
return todo.id !== id
})
},
// 勾选or取消勾选所有todo
checkAllTodo (done) {
this.todos.forEach((todo) => {
todo.done = done
})
},
// 清除所有已完成的todo
clearAllTodo () {
this.todos = this.todos.filter((todo) => {
return !todo.done
})
}
},
mounted () {
// 事件总线上绑定事件
this.$bus.$on('checkTodo', this.checkTodo)
this.$bus.$on('updateTodo', this.updateTodo)
// 订阅消息
this.pubId = pubsub.subscribe('deleteTodo', this.deleteTodo)
},
beforeDestroy () {
// 组件销毁时,销毁总线上的事件
this.$bus.$off(['checkTodo', 'updateTodo'])
// 取消订阅
pubsub.unsubscribe(this.pubId)
},
// 1. 将最新的数据存到localStorage,数据会发生增删改查,只需监视todos数据,有变化时将新的重新存入即可
watch: {
todos: {
// 2. 开启深度监视,todos是个对象数组,不开启深度监视,当某一个对象的元素发生变化时,watch监听不到
deep: true,
handler (value) {
localStorage.setItem('todos', JSON.stringify(value))
}
}
}
</script>