【13.Vue的消息订阅与发布
】
- 备注:全局事件总线用的更多些,消息订阅与发布只需了解即可。
- 【注意点1】:由于“消息订阅与发布”可依赖的第三方库太多了,这里使用pubsub-js
问题:“全局事件总线”和“消息订阅与发布”都可以实现任意组件间通信,那用哪个好?
答案:推荐使用“全局事件总线”,因为它是vue提供的,完全使用的vue技术,而“消息订阅与发布”则是第三方。
- 消息订阅与发布的作用:一种组件间通信的方式,适用于任意组件间通信【与数据总线的作用相似】。
图解消息发布与订阅
【举例】C组件想给A组件传递数据:
- A组件是订阅者,A订阅一个名叫demo的消息,并设置一个test回调函数;C组件是发布者,C会发布一个名叫demo的消息,并将C组件里面的数据传递出去。如上图所示,一旦C发布了demo消息【附带传递的数据,‘888’】,就会让A组件收到订阅消息,并执行回调函数test,并且来接收C传递过来的数据。【注意:回调函数test收到的第一个参数是订阅的消息名,第二个参数才是收到的data】
Student组件给School组件传递数据的使用步骤:
-
安装pubsub:
npm i pubsub-js
(安装不成功、使用管理员权限运行) -
引入pubsub-js库【该库安装在node_modiles文件中】:
import pubsub from 'pubsub-js'
(订阅和发布都要引入) -
订阅者订阅:
School.vue文件中
// 挂载 mounted() { // 订阅消息:haha 回调函数:箭头函数 // params:接收的参数【传过来的数据是一个数组】 //下划线【_】:接受的参数是haha[订阅的消息] this.pubId = pubsub.subscribe('haha', (_,params) => { console.log("收到的第一个参数",params[0]); //haha console.log("收到的第二个参数",params[1]); //何大春 this.StudentAge=params[1] this.StudentName=params[0] }); },
-
发布者发布:
Student.vue文件中
methods: { SendStudentData(){ // 发布订阅消息haha,并传递数据params const params =[this.name,this.age] pubsub.publish('haha',params) } },
-
订阅者取消订阅:
School.vue文件中
// 销毁前取消订阅 beforeDestroy() { // 取消订阅 pubsub.unsubscrib(this.pubId) },
完整代码:
-
main.js文件中需要修改的代码
//引入Vue import Vue from 'vue' //引入App import App from './App.vue' //关闭Vue的生产提示 Vue.config.productionTip = false //创建vm new Vue({ el:'#app', render: h => h(App), })
-
App.vue文件中需要修改的代码
<template> <div> <School/> <br/> <Student/> </div> </template> <script> import School from "./components/School"; import Student from './components/Student'; export default { name: 'App', components: {School,Student}, }; </script>
-
School.vue文件
<template> <div class="school"> <h1>School组件</h1> <h2>兄弟组件Student传过来的name:{{ StudentName }}</h2> <h2>兄弟组件Student传过来的age:{{ StudentAge }}</h2> </div> </template> <script> import pubsub from 'pubsub-js'; export default { name: 'School', data() { return { StudentName: '', StudentAge: '', }; }, // 挂载 mounted() { // 订阅消息:haha 回调函数:箭头函数 // params:接收的参数 this.pubId=pubsub.subscribe('haha', (_,params) => { console.log("收到的第一个参数",params[0]); //haha console.log("收到的第二个参数",params[1]); //何大春 console.log("收到的params参数",params); //何大春 this.StudentAge=params[1] this.StudentName=params[0] }); }, // 销毁前取消订阅 beforeDestroy() { // 取消订阅 pubsub.unsubscrib(this.pubId) }, }; </script> <style scoped> .school { background-color: rgb(73, 192, 150); } </style>
-
Student.vue文件
<template> <div class="student"> <h1>Student组件信息</h1> <h2>学生姓名:{{ name }}</h2> <h2>学生性别:{{ sex }}</h2> <h2>学生年龄:{{ age }}</h2> <h2>学生成绩:{{ score }}</h2> <button class="haha" @click="SendStudentData"> <h2>点击此处给兄弟组件School传值</h2></button> </div> </template> <script> import pubsub from 'pubsub-js' export default { name: 'Student', data() { return { name: '何大春', sex: '男', age: '22', score: '88', }; }, methods: { SendStudentData(){ // 发布订阅消息haha,并传递数据 const params =[this.name,this.age] pubsub.publish('haha',params) } }, }; </script> <style lang="less" scoped> .student { background-color: tomato; padding: 50px; margin-top: 50px; margin-left: 50px; width: 300px; height: 350px; } .h2 { padding: 5px; margin: 5px 5px 5px 5px; } .haha { background-color: rgb(211, 233, 130); } </style>
补充知识点:
注意1:“取消订阅方式和“全局事件总线”不同,取消订阅得指定订阅返回的id,且每次返回的id都不同,而“全局事件总线”指定的是“自定义事件名称”
this.pubId = pubsub.subscribe('订阅消息名',(_,params)=>{ })
beforeDestroy() {
// 取消订阅
pubsub.unsubscrib(this.pubId)
},
注意2:订阅【pubsub.subscribe()】回调配置一定要使用箭头函数【如上面的代码】或者外部定义方法【将回调函数配置在methods中,再去订阅中引用】,在订阅中引用也行,千万不要使用普通函数,因为普通函数中this不指代vc,而是undefind。
注意3:消息订阅会接收到2个参数,第1个参数为消息名称,第2个参数才是传递过来的值,但是实际msgName参数1他跟用不到它,所以可使用下划线“_”占个位。【写法如下】
this.pubId = pubsub.subscribe('hello',(msgName,data)=>{
// console.log('有人发布了hello消息,hello消息的回调执行了',msgName,data)
})
注意4:如果想传递多个参数,需使用对象的形式{}
或数组的形式[]
【写法如下】
发送方
// 发布订阅消息haha,并传递数据
// 方法一:利用数组传递多个数据
// const paramsArray =[this.name,this.age]
// pubsub.publish('haha',paramsArray)
// 方法二:利用对象传递多个数据
const paramsObeject = {name:this.name,age:this.age}
pubsub.publish('haha',paramsObeject)
接收方
// 挂载
mounted() {
// 订阅消息:haha 回调函数:箭头函数
// params:接收的参数
/* this.pubId=pubsub.subscribe('haha', (_,params) => {
console.log("收到的第一个参数",params[0]); //haha
console.log("收到的第二个参数",params[1]); //何大春
console.log("收到的params参数",params); //何大春
this.StudentAge=params[1]
this.StudentName=params[0]
}); */
// 方法二:利用对象传递多个数据
this.pubId=pubsub.subscribe('haha', (_,paramsObeject) => {
console.log("对象的name参数",paramsObeject.name); //haha
console.log("对象的age参数",paramsObeject.age); //何大春
console.log("收到的params参数",paramsObeject); //{name: '何大春', age: '22'}
this.StudentAge=paramsObeject.age
this.StudentName=paramsObeject.name
});
},
【14.TodoList案例之消息订阅与发布
】
-
备注:主要是将之前利用全局事件总线来实现组件间的通信,改为利用消息订阅与发布来实现组件间的通信。
-
App.vue文件中需要修改的代码
原本
: Todo案例中TodoItem给App组件传递数据【通信】的方法:利用全局事件总线去通信
<TodoList :todos="todos" />
mounted() { //勾选or取消勾选一个todo this.$bus.$on('checkTodo',(id)=>{ this.todos.forEach((todo) => { if(todo.id === id) todo.done = !todo.done }) }) this.$bus.$on('deleteTodo',(id)=>{ // 删除一个todo // deleteTodo里面的id指的是:点击事件对应的id console.log('deleteTodo',id); this.todos=this.todos.filter( (todo)=>{ return todo.id != id } ) }) }, beforeDestroy() { this.$bus.$off(['deleteTodo','checkTodo']) },
修改后:
Todo案例中TodoItem给App组件传递数据【通信】的方法:利用消息的订阅与发布去通信
<TodoList :todos="todos" />
mounted() { // 利用数据总线去通信 //勾选or取消勾选一个todo this.$bus.$on('checkTodo',(id)=>{ this.todos.forEach((todo) => { if(todo.id === id) todo.done = !todo.done }) }) // 利用消息的订阅与发布去通信 // 订阅deleteTodo this.pubId = pubsub.subscribe('deleteTodo',(_,id)=>{ this.todos=this.todos.filter( (todo)=>{ return todo.id != id } ) }) }, beforeDestroy() { // this.$bus.$off(['deleteTodo','checkTodo']) this.$bus.$off(['checkTodo']) // 取消订阅 pubsub.unsubscribe(this.pubId) },
-
TodoItem.vue文件中需要修改的代码
原本
: Todo案例中TodoItem给App组件传递数据【通信】的方法:利用全局事件总线去通信
//点击后,自定义事件deleteTodo触发后通知App组件将对应的todo对象删除,传参数 this.$bus.$emit('deleteTodo',this.todo.id)
修改后:
Todo案例中TodoItem给App组件传递数据【通信】的方法:利用消息的订阅与发布去通信
//点击后,发布订阅后通知App组件接收订阅消息deleteTodo和参数this.todo.id pubsub.publish('deleteTodo',this.todo.id)
完整代码:
main.js
//引入Vue
import Vue from 'vue'
//引入App
import App from './App.vue'
//关闭Vue的生产提示
Vue.config.productionTip = false
//创建vm
new Vue({
el:'#app',
render: h => h(App),
// 生命周期钩子beforeCreate中模板未解析,且this是vm
beforeCreate() {
// this:指的是vm
Vue.prototype.$bus = this //安装全局事件总线$bus
}
})
App.vue
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<TodoHeader @addTodo="addTodo"/>
<TodoList
:todos="todos"
/>
<TodoFooter
:todos="todos"
@checkAllTodo="checkAllTodo"
@clearAllTodo="clearAllTodo"
/>
</div>
</div>
</div>
</template>
<script>
import pubsub from "pubsub-js";
//引入App的子组件
import TodoHeader from './components/TodoHeader'
import TodoFooter from './components/TodoFooter'
import TodoList from './components/TodoList'
export default {
name:'App',
components: { TodoHeader,TodoFooter,TodoList },
data() {
return {
//由于todos是TodoHeader组件和TodoFooter组件都在使用,所以放在App中(状态提升)
todos:[
{id:'001',title:'练功夫',done:true},
{id:'002',title:'睡觉',done:false},
{id:'003',title:'打豆豆',done:true}
],
}
},
methods: {
//添加一个todo
addTodo(todoObj){
// 在数组的开头添加一个数据
this.todos.unshift(todoObj)
},
//全选or取消全选
checkAllTodo(done){
this.todos.forEach(todo => todo.done = done)
},
// 清除所有已经完成的todo
clearAllTodo(){
this.todos= this.todos.filter(todo =>{
return todo.done == false
// 或者换成 return !todo.done
}
)
},
},
mounted() {
// 利用数据总线去通信
//勾选or取消勾选一个todo
this.$bus.$on('checkTodo',(id)=>{
this.todos.forEach((todo) => {
if(todo.id === id) todo.done = !todo.done
})
})
// 利用消息的订阅与发布去通信
// 订阅deleteTodo
this.pubId = pubsub.subscribe('deleteTodo',(_,id)=>{
this.todos=this.todos.filter(
(todo)=>{
return todo.id != id
}
)
})
},
beforeDestroy() {
// this.$bus.$off(['deleteTodo','checkTodo'])
this.$bus.$off(['checkTodo'])
// 取消订阅
pubsub.unsubscribe(this.pubId)
},
}
</script>
<!-- style没有scoped属性:【全局样式】 -->
<!-- style有scoped属性:样式设置只在本组件里起作用【局部样式】 -->
<style>
/*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;
/* 上下外边距为0,左右自动,实际效果为左右居中*/
margin: 0 auto;
}
/* 后代选择器(包含选择器),选择到的是todo-container下面的所有后代todo-wrap */
.todo-container .todo-wrap {
padding: 10px;
border: 1px solid #67dbd1;
border-radius: 5px;
}
</style>
TodoHeader.vue
<template>
<div class="todo-header">
<!-- @keyup.enter="add" :按下回车按键,调用add方法 -->
<input type="text" placeholder="请输入你的任务名称,按回车键确认" @keyup.enter="add" v-model="title"/>
</div>
</template>
<script>
// 引入nanoid库生成ID号
import { nanoid } from 'nanoid'
export default {
name: 'TodoHeader',
/* //接收从App组件【父组件】传递过来的addTodo方法
props:['addTodo'], */
data() {
return {
title: '',
}
},
methods: {
add(){
// 如果输入框里为空,就跳过下面的代码,并弹窗
if (!this.title.trim()) return alert('请输入值')
//将用户的输入包装成一个todo对象
const todoObj={id:nanoid(),title:this.title,done:false}
//通知App组件去添加一个todo对象
//触发自定义事件addTodo,并把子组件中的参数todoObj传给父组件
this.$emit('addTodo',todoObj)
//清空输入
this.title = ''
}
},
};
</script>
<style scoped>
/* 头部样式设置 */
/* 后代选择器(包含选择器),选择到的是todo-header下面的所有后代input */
.todo-header input {
width: 560px;
height: 28px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 4px;
/* 内边距:上下4px,左右7px */
padding: 4px 7px;
}
/* :focus获得焦点,并设置其新样式:例如:用户单击一个input输入框获取焦点,然后这个input输入框的边框样式就会发生改变,和其他的输入框区别开来,表明已被选中。 */
.todo-header input:focus {
/* outline 与 border 相似,不同之处在于 outline 在整个元素周围画了一条线;它不能像 border 那样,指定在元素的一个面上设置轮廓,也就是不能单独设置顶部轮廓、右侧轮廓、底部轮廓或左侧轮廓。 */
outline: none;
/* 定义边框的颜色 */
border-color: rgba(82, 168, 236, 0.8);
/* boxShadow 属性把一个或多个下拉阴影添加到框上 */
/* 设置inset:内部阴影,不设置inset:外部阴影 */
/* 【0 0】:不设置X轴与Y轴偏移量 */
/* 第三个值【如10px,8px】:设置值阴影模糊半径为15px */
box-shadow: inset 0 0 10px rgba(124, 56, 207, 0.075), 0 0 8px rgba(224, 58, 17, 0.6);
background-color: bisque;
}
</style>
TodoList.vue
<template>
<ul class="todo-main">
<TodoItem
v-for="todoObj in todos"
:key="todoObj.id"
:todo="todoObj"
/>
</ul>
</template>
<script>
import TodoItem from './TodoItem'
export default {
name: 'TodoList',
components:{TodoItem},
//声明接收App传递过来的数据,其中todos是自己用的,checkTodo和deleteTodo是给子组件TodoItem用的
props: ['todos']
};
</script>
<style scoped>
/*main*/
.todo-main {
/* 左外边距:0px 【盒子贴着盒子】*/
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>
TodoItem.vue
<template>
<li>
<label>
<input type="checkbox"
:checked="todo.done"
@change="handleCheck()"
/>
<span>{{todo.title}}</span>
</label>
<button class="btn btn-danger" @click="handleDelete()">删除</button>
</li>
</template>
<script>
import pubsub from "pubsub-js";
export default {
name: 'TodoItem',
// 声明接受从别的组件中的todoObj对象,todo
props: ['todo'],
methods: {
//勾选or取消勾选【别弄混了:这里的id其实就是上面change事件中的todo.id】
handleCheck(){
//change事件触发后,通知App组件将对应的todo对象的done值取反
// this.checkTodo(id)
this.$bus.$emit('checkTodo',this.todo.id)
// console.log(this.todo.id);
},
//删除
handleDelete(){
if (confirm('Are you sure you want to delete?')) {
//点击后,发布订阅后通知App组件将对应的todo对象删除,参数
pubsub.publish('deleteTodo',this.todo.id)
}
}
},
};
</script>
<style scoped>
/*item*/
li {
/* ul无序列表 ol有序列表*/
/* 列表前面无标记 */
list-style: none;
/* height定义了一个li盒子的高度 */
height: 36px;
/* 行高:指的是文字占有的实际高度 */
line-height: 36px;
/* 当height和line-height相等时,即盒子的高度和行高一样,内容上下居中 */
padding: 0 5px;
/* 边框底部:1px的实心线 颜色*/
border-bottom: 1px solid #c0abc3;
}
/* 后代选择器(包含选择器),选择到的是li下面的所有后代label */
li label {
/* 左对齐浮动【元素一旦浮动就会脱离文档流(不占位,漂浮)】 */
float: left;
/* 鼠标放在label元素上时变成小小手 */
cursor: pointer;
}
li label input {
/* 垂直居中 */
vertical-align: middle;
margin-right: 6px;
position: relative;
top: -1px;
}
/* 后代选择器(包含选择器),选择到的是li下面的所有后代button */
li button {
/* 向右浮动 */
float: right;
/* 不为被隐藏的对象保留其物理空间,即该对象在页面上彻底消失,通俗来说就是看不见也摸不到。 */
display: none;
/* 上边距为3px */
margin-top: 3px;
}
li:before {
/* initial:它将属性设置为其默认值。 */
content: initial;
}
/* 结构伪类选择器 选择最后一个li元素 */
li:last-child {
/* 边框底部没有线 */
border-bottom: none;
}
li:hover{
background-color: #ddd;
}
/* 鼠标移动到该元素上时,将button按钮显示出来 */
li:hover button{
/* display:block将元素显示为块级元素 */
display: block;
}
</style>
TodoFooter.vue
<template>
<div class="todo-footer" v-show="total">
<label>
<!-- <input type="checkbox" :checked="isAll" @change="checkAll"/>
可以用下面这行代码代替-->
<!--对于type="checkbox",v-model绑定的就是一个布尔值,isAll为false or true -->
<input type="checkbox" v-model="isAll"/>
</label>
<span>
<span>已完成{{ doneTotal }}</span> / 全部{{ total }}
</span>
<button class="btn btn-danger" @click="clearAllDone">清除已完成任务</button>
</div>
</template>
<script>
export default {
name: 'TodoFooter',
props: ['todos'],
computed:{
//总数
total(){
return this.todos.length
},
// 已完成数
doneTotal(){
//此处使用reduce方法做条件统计
/* return this.todos.reduce(
// todo:遍历数组todos中的每一个元素
// 比如:数组遍历时,把数组todos中的每一个元素分别赋值给todo【包含id='001'】
(pre,todo)=>{
// console.log('@',pre,todo)
return pre + (todo.done ? 1 : 0)
}
,0) */
//简写
return this.todos.reduce((pre,todo)=>pre + (todo.done ? 1 : 0),0)
},
//控制全选框
/* isAll(){
//计算属性简写:isAll属性,只能被读取,不能被修改
return this.total === this.doneTotal && this.total>0
} */
isAll:{
//get有什么作用?当有人读取isAll时,get就会被调用,且返回值就作为isAll的值
//get什么时候调用?1.初次读取isAll时。2.所依赖的数据发生变化时。
get(){
//全选框是否勾选 【&&:且】
return this.total === this.doneTotal && this.total>0
},
//set什么时候调用? 当isAll被修改时。
// value就是:v-model绑定的值false【未勾选】 or true【勾选】
set(value){
console.log(value)
this.$emit('checkAllTodo',value)
}
},
},
methods: {
/* checkAll(e){
console.log(e.target.checked);
// 拿到的是全选或者全不选的布尔值
this.checkAllTodo(e.target.checked)
} */
// 清空所有已完成
clearAllDone(){
// this.clearAllTodo()
this.$emit('clearAllTodo')
}
},
};
</script>
<style 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>