🥔:高度自律即自由
更多Vue知识请点击——Vue.js
VUE2-Day9
- 全局事件总线
- 1、安装全局事件总线
- 2、使用事件总线
- (1)接收数据
- (2)提供数据
- (3)组件销毁前最好解绑
- 3、TodoList中的孙传父
- (1)首先在main.js中安装全局事件总线
- (2)在App.vue中绑定全局自定义事件,并使用之前写好的回调
- (3)Item中触发事件,并把数据id传过去
- 消息的订阅与发布
- (1)使用消息的订阅与发布
- (2)TodoList案例使用消息的订阅与发布
- TodoList的编辑功能
- 1、整体思路
- 2、给标签添加事件
- 3、点击编辑按钮切换span为input
- 4、失去焦点传数据
- 5、App收数据
- $nextTick
- 动画与过渡
- 进入离开动画三种写法
全局事件总线
一种组件间通信的方式,适用于任意组件间通信。通俗理解就是一个定义在所有组件之外的公共嘎达,这个嘎达可以有vm或vc上的同款$on、$off、$emit
,也可以让所有组件都访问到。要想实现这个事情,只能在Vue.prototype
上添加一个属性,值是vm或vc
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q3OYNFD4-1692378989491)(D:\wxf\前端学习笔记\vue2+vue3\笔记图片\全局事件总线.png)]
vm.$emit( event, arg ) //触发当前实例上的事件,arg是传递给父组件的参数
vm.$on( event, fn ) //监听event事件后运行 fn
$off(type, fn) //注销消息方法 type:消息名称 fn:消息回调函数
1、安装全局事件总线
安装全局事件总线可以用vc也可以用vm,写在main.js里面
- 用vc的话这么写:
const Demo = Vue.extend({});
const d = new Demo();
Vue.prototype.$bus = d;
- 用vm的话这么写(我们通常用vm):
new Vue({
el: '#app',
render: (h) => h(App),
//放这个函数里,是因为模板还未解析,这个函数在自定义事件定义之前,是不会报错滴
beforeCreate() {
Vue.prototype.$bus = this //安装全局事件总线
},
})
在这里我们使用vm安装全局事件总线。
2、使用事件总线
(1)接收数据
接收数据:A组件想接收数据,则在A组件中给$bus
绑定自定义事件,事件的回调留在A组件自身。
methods(){
demo(data){......}
}
......
mounted() {
this.$bus.$on('xxxx',this.demo)
}
(2)提供数据
任意一个组件,都可以给上面说的A组件传数据
methods:{
sendStudentName(){
this.$bus.$emit('xxxx',this.name)
}
}
(3)组件销毁前最好解绑
最好在beforeDestroy钩子中,用`$of去解绑当前组件所用到的事件。
因为接收数据的组件A中定义的回调函数和自定义事件是绑定的,而这个用来接收数据的组件实例A都销毁了,回调函数也没了,那这个自定义事件也就没用了,你留着会污染全局环境。
beforeDestroy() {
this.$bus.$off('hello')
}
3、TodoList中的孙传父
之前我们孙传父都是父亲传给儿子函数,儿子传给孙子函数,然后孙子再调用函数传值,很麻烦,但是现在我们可以用全局事件总线实现孙子给父亲传数据。此处只展示修改部分,记得把之前方法的相关代码删掉或注释掉。
(1)首先在main.js中安装全局事件总线
new Vue({
el: '#app',
render: h => h(App),
beforeCreate() {
Vue.prototype.$bus = this; //创建全局事件总线
}
});
(2)在App.vue中绑定全局自定义事件,并使用之前写好的回调
mounted() {
//挂载完成后给全局事件总线添加事件
this.$bus.$on('changeTodo', this.changeTodo);
this.$bus.$on('deleteTodo', this.deleteTodo);
},
beforeDestroy() {
//最好在销毁前解绑
this.$bus.$off(['changeTodo', 'deleteTodo']);
},
(3)Item中触发事件,并把数据id传过去
handleChange(id) {
//触发全局事件总线中的事件
this.$bus.$emit('changeTodo', id);
},
handleDelete(id) {
if (confirm('确定要删除吗?')) //点确定是true,取消是false
this.$bus.$emit('deleteTodo', id);
}
消息的订阅与发布
(1)使用消息的订阅与发布
消息的订阅与发布用的不多,和全局事件总线写法差不多,但是全局事件总线更好,因为是在Vue身上操作,但是这个的话要引入第三方库,库有很多,比如pubsub-js
安装pubsub:npm i pubsub-js
引入:import pubsub from 'pubsub-js
接收数据:A组件想接收数据,则在A组件中订阅消息,订阅的回调留在A组件自身。
接收两个参数,第一个是消息名字,第二个是传过来的数据
methods(){
demo(msgName,data){......}
}
......
mounted() {
//订阅消息
this.pubsubId = pubsub.subscribe('xxx',this.demo)
},
beforeDestroy() {
//销毁时取消订阅
pubsub.unsubscribe(this.pubsubId);
},
提供数据:
methods: {
sendStudentName() {
// this.$bus.$emit('hello', this.name);
//发布消息并传数据
pubsub.publish('hello', this.name);
}
},
可以对比一下前面的全局事件总线写法,个人认为写法差别不大。
可以尝试把TodoList案例里孙传父使用全局事件总线的写法改成使用消息订阅与发布试试。
(2)TodoList案例使用消息的订阅与发布
这里把**deleteTodo
改成消息订阅与发布,changeTodo
依旧使用全局事件总线**,大家可以仔细对比他们的区别。
- App.vue(订阅消息/接收数据):
mounted() {
//挂载完成后给全局事件总线添加事件
this.$bus.$on('changeTodo', this.changeTodo)
// this.$bus.$on('deleteTodo', this.deleteTodo)
//deleteTodo换一种方法 用订阅消息写
this.pubsubIs = pubsub.subscribe('deleteTodo', this.deleteTodo)
},
beforeDestroy() {
//最好在销毁前解绑
// this.$bus.$off(['changeTodo', 'deleteTodo'])
//使用全局事件总线解绑
this.$bus.$off('changeTodo')
//deleteTodo换一种方法 用订阅消息解绑
pubsub.unsubscribe(this.pubsubId)
},
这里别忘了这个subscribe里的回调,接收两个参数,第一个是消息名,后面才是数据,所以deleTodo方法得加个参数
//虽然msgName没用上,但是如果不加就会把id当第一个参数,id就变成msgName,所以这里必须要加,或者直接拿一个下划线_占位也行
deleteTodo(msgName, id) {
this.todos = this.todos.filter(todo => todo.id !== id);
},
- Item.vue(发布消息/提供数据)
handleChange(id) {
//使用全局事件总线
//触发全局事件总线中的事件
this.$bus.$emit('changeTodo', id);
},
handleDelete(id) {
//confirm点确定是true,取消是false
if (confirm('确定要删除吗?'))
//使用消息订阅与发布发布消息
pubsub.publish('deleteTodo', id); //发布消息
}
TodoList的编辑功能
1、整体思路
首先得有一个按钮,点击之后出现input框,框里是todo.title,而且原来的span要隐藏。然后修改完之后,失去焦点会自动更新数据,并且span出现,input框隐藏。
除此之外还有个细节,那就是点击编辑要自动获取焦点,要不然会有一个小问题(点击编辑,然后突然不想改了,还得手动点一下input,再点下别的地方,才会变回span)
想实现span和input的来回切换,就要给todo添加新的属性,用来标识这个变换,这里起名叫isEdit
所以大致思路:给标签添加事件 => 点击编辑按钮切换span为input => 失去焦点传数据 => App收数据 => 解决焦点bug
2、给标签添加事件
(1)isEdit一上来是没有的,所以todo.isEdit = false,再加上默认上来显示的是span,所以span加个v-show=“!todo.isEdit”,input加个v-show=“todo.isEdit” ,button加v-show="!todo.isEdit"是因为我们一般编辑时,这个按钮应该消失才对,所以和span一致
(2)由于props接过来的数据不能改,所以使用单向数据绑定:value=“todo.title”
(3)ref=“inputTitle” 是为了方便后面拿到input元素然后操作它写的(nextTick)
(4)@blur="handleBlur(todo, $event)"是失去焦点时触发的事件,用来给App传值
<span v-show="!todo.isEdit">{{ todo.title }}</span>
<input
type="text"
v-show="todo.isEdit"
:value="todo.title"
ref="inputTitle"
@blur="handleBlur(todo, $event)">
<button class="btn btn-edit" @click="handleEdit(todo)" v-show="!todo.isEdit">编辑</button>
给这个编辑按钮加个样式:
.btn-edit {
color: #fff;
background-color: rgb(13, 166, 13);
border: 1px solid green;
margin-right: 5px;
}
.btn-edit:hover {
color: #fff;
background-color: green;
}
3、点击编辑按钮切换span为input
点击编辑给todo追加属性,用来切换span为input。这里考虑到给todo追加属性的问题,如果想要让Vue监测到这个属性,那么必须使用$set来添加isEdit,且默认值为true(因为编辑的时候显示的是input啊,想想v-show="todo.isEdit"
)。
但是这里边有点儿问题,如果已经添加过了isEdit,那每次点击编辑按钮,都会添加一次isEdit属性,这样是不太好的,所以要加个判断,添加过了就改成true,没添加过就添加个true
handleEdit(todo) {
if (todo.isEdit !== undefined) {
console.log('todo里有isEdit属性了')
todo.isEdit = true;
} else {
console.log('todo里没有isEdit属性')
this.$set(todo, 'isEdit', true);
}
this.$nextTick(function () {
this.$refs.inputTitle.focus();
})
},
4、失去焦点传数据
失去焦点首先input得变回span,然后使用全局事件总线传值,传值一定要传当前input框的value值,因为这才是你修改后的值,使用事件对象获取。(当然,别忘了id也要传)
handleBlur(todo, e) {
todo.isEdit = false;
if (!e.target.value.trim()) return alert('值不能为空!'); //trim去掉空格
this.$bus.$emit('updateTodo', todo.id, e.target.value);
}
5、App收数据
把input框里你写的东西拿过来,给对应的todo.title
//6.实现编辑todo
methods: {
......
updateTodo(id, title) {
this.todos.forEach((todo) => {
if (todo.id === id) { todo.title = title }
});
}
}
mounted() {
......
//实现编辑功能,接收数据
this.$bus.$on('updateTodo', this.editTodo);
},
beforeDestroy() {
//最好在销毁前解绑
this.$bus.$off('updateTodo');
},
$nextTick
1、语法:this.$nextTick(回调函数)
2、作用:在下一次 DOM 更新结束,v-for循环结束后
执行其指定的回调。
3、什么时候用:当改变数据后,要基于更新后的新DOM进行某些操作时(如input自动获取焦点),要在nextTick所指定的回调函数中执行。
4、比如刚才点击编辑后,input框没法自动获取焦点,那么我就得先点一下,再点别处儿才能切换回去,如果直接this.$refs.inputTitle.focus();
不行,因为这个函数虽然动了isEdit的值,但是模板重新解析也得等这个函数走完啊,那input还没创建出来呢,就focus了,肯定是不行滴。
有个办法就是用异步,也可以解决,但是更好的办法是$nextTick
动画与过渡
1、作用:在插入、更新或移除 DOM元素时,在合适的时候给元素添加样式类名。
2、写法:
准备好样式:
元素进入的样式:
v-enter:进入的起点
v-enter-active:进入过程中
v-enter-to:进入的终点
元素离开的样式:
v-leave:离开的起点
v-leave-active:离开过程中
v-leave-to:离开的终点
使用<transition>
包裹要过度的元素,并配置name属性:
<transition name="hello">
<h1 v-show="isShow">你好啊!</h1>
</transition>
3、备注:若有多个元素需要过度,则需要使用:<transition-group>
,且每个元素都要指定key值。
进入离开动画三种写法
- 1、动画(Test.vue)
<template>
<div>
<button @click="isShow = !isShow">显示/隐藏</button>
<transition name="hello" appear>
<h1 v-show="isShow">你好呀!</h1>
</transition>
</div>
</template>
<script>
export default {
name: 'Test',
data() {
return {
isShow: true,
}
},
}
</script>
<style scoped>
h1 {
background-color: pink;
}
.hello-enter-active {
animation: potato 1s;
}
.hello-leave-active {
animation: potato 1s reverse;
}
@keyframes potato {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0px);
}
}
</style>
- 2、过渡动画(Test2.vue)
<template>
<div>
<button @click="isShow = !isShow">显示/隐藏</button>
<transition-group name="hello" appear>
<h1 v-show="isShow" key="1">你好呀!</h1>
<h1 v-show="isShow" key="2">小土豆</h1>
</transition-group>
</div>
</template>
<script>
export default {
name: 'Test2',
data() {
return {
isShow: true,
}
},
}
</script>
<style scoped>
h1 {
background-color: skyblue;
}
/* 进入的起点,离开的终点 */
.hello-enter,
.hello-leave-to {
transform: translateX(-100%);
}
/* 进入的终点,离开的起点 */
.hello-enter-to,
.hello-leave {
transform: translateX(0);
}
.hello-enter-active,
.hello-leave-active {
transition: 0.5s linear;
}
</style>
- 3、集成第三方动画
<template>
<div>
<button @click="isShow = !isShow">显示/隐藏</button>
<transition-group
name="animate__animated animate__bounce"
appear
enter-active-class="animate__swing"
leave-active-class="animate__backOutUp"
>
<h1 v-show="isShow" key="1">你好呀!</h1>
<h1 v-show="isShow" key="2">小土豆</h1>
</transition-group>
</div>
</template>
<script>
import 'animate.css'
export default {
name: 'Test3',
data() {
return {
isShow: true,
}
},
}
</script>
<style scoped>
h1 {
background-color: orange;
}
</style>
- App.vue
<template>
<div id="App">
<Test />
<Test2 />
<Test3 />
</div>
</template>
<script>
import Test from './components/Test'
import Test2 from './components/Test2'
import Test3 from './components/Test3'
export default {
name: 'App',
components: { Test, Test2, Test3 },
}
</script>
- 效果:(从上往下分别是Test、Test2、Test3效果)