通过生活中报纸的订阅与发布,引出本节内容:消息订阅与发布
报纸订阅与发布
1)订阅报纸:住址
2)邮递员送报纸:报纸
消息订阅与发布
1)订阅消息:消息名
2)发布消息:消息内容
上述App中,组件C中的数据想要传递给组件A(A是接收数据的,C是提供数据的),在A中订阅一个消息,消息名为demo,回调函数是test; 在C中发布一个demo消息,参数为666,通过以上的消息订阅与发布操作,完成组件C中的参数666回调到组件A的test函数
消息订阅与发布(pubsub)是一种组件间通信的方式,适用于任意组件间通信
使用步骤:
1. 安装 pubsub-js库
npm install pubsub-js
2. 引入 pubsub-js库
import pubsub from "pubsub-js";
3. 接收数据:A组件想接收数据,则在A组件中订阅消息,订阅的回调留在A组件自身
methods: {
demo(msgName, data) {
// msgName:消息名 data:传递的数据
console.log("有人发布了hello消息,hello消息的回调执行了", msgName, data)
}
},
...
mounted() {
// 消息订阅(接收数据)
// this.pudId = pubsub.subscribe("hello",(msgName,data)=>{
// // msgName:消息名 data:传递的数据
// console.log("有人发布了hello消息,hello消息的回调执行了", msgName, data)
// })
this.pubId=pubsub.subscribe("hello", this.demo)
}
4. 提供数据:
// 消息发布(提供数据)
pubsub.publish("hello", 666)
5. 最好在beforeDestroy钩子中,用pubsub.unsubscribe(pid) 去取消订阅
beforeDestroy() {
// 取消订阅(通过this.pudId取消订阅)
pubsub.unsubscribe(this.pubId)
}
案例:使用消息订阅与发布的方式,把Student组件的学生姓名属性,传递给School组件,并进行替换
src/components/School.vue(接收数据,消息订阅)
<template> <div class="school"> <h2>学校名称:{{name}}</h2> <h2>学校地址:{{address}}</h2> </div> </template> <script> import pubsub from "pubsub-js"; export default{ // eslint-disable-next-line vue/multi-word-component-names name:"School", data(){ return{ name:"马铃薯的博客园", address:"河北省邯郸市" } }, methods:{ demo(msgName,data){ // msgName:消息名 data:传递的数据 console.log("有人发布了hello消息,hello消息的回调执行了", msgName, data) this.name = data } }, // 生命周期 mounted , 这时呈现的是经过 Vue编译的 DOM mounted() { console.log("School",this) // 监听当前实例上自定义事件hello // this.$bus.$on("hello",(data)=>{ // console.log("我是School组件,收到了数据:",data) // this.name = data // }) // 消息订阅(接收数据) // this.pudId = pubsub.subscribe("hello",(msgName,data)=>{ // // msgName:消息名 data:传递的数据 // console.log("有人发布了hello消息,hello消息的回调执行了", msgName, data) // }) this.pubId = pubsub.subscribe("hello",this.demo) console.log("@@",this.pubId) // uid_0,每次订阅一个消息,就是一个新的对象 }, beforeDestroy() { // 解绑自定义事件 hello // this.$bus.$off("hello") // 取消订阅(通过this.pudId取消订阅) pubsub.unsubscribe(this.pubId) } } </script> <style scoped> .school{ background-color: skyblue; padding: 5px; } </style>
src/components/Student.vue(提供数据,消息发布)
<template> <div class="student"> <h2>学生姓名:{{name}}</h2> <h2>学生性别:{{sex}}</h2> <button @click="sendStudentName">把学生姓名给School组件</button> </div> </template> <script> // 导入 pubsub-js 库 import pubsub from "pubsub-js" export default{ // eslint-disable-next-line vue/multi-word-component-names name:"School", data(){ return{ name:"马铃薯", sex:"男", } }, mounted() { console.log("Student",this) }, methods:{ sendStudentName() { // 触发当前实例上的自定义事件 hello // this.$bus.$emit("hello" ,this.name) // 消息发布(提供数据) pubsub.publish("hello", this.name) } } } </script> <style scoped> .student{ background-color: pink; padding: 5px; margin-top: 30px; } </style>
src/App.vue(无变动)
<template> <div class="app"> <h1>{{msg}}</h1> <School></School> <Student></Student> </div> </template> <script> import Student from "@/components/Student.vue"; import School from "@/components/School.vue" export default{ name:"App", data(){ return{ msg:"你好啊" } }, components:{ Student:Student, School:School }, } </script> <style> .app{ background-color: gray; padding: 5px; } </style>
src/main.js(无变动,不使用事件总结的方式)
import Vue from "vue"
import App from "./App.vue"
// 阻止 vue 在启动时生成生产提示
Vue.config.productionTip = false
new Vue({
el:"#app",
render:h => h(App),
// 生命周期 beforeCreate 这时vue还未解析模板,初始化的数据监测、数据代理还未开始
beforeCreate() {
// 安装全局事件总线
// Vue.prototype.$bus = this
}
})
使用消息订阅与发布的方法,优化Todo-List案例(优化UserItem中的handleDelete删除功能,以及对应App的deleteTodo)
src/components/UserHeader.vue(无改动)
<template> <div class="todo-header"> <input type="text" placeholder="请输入你的任务名称,按回车键确认" v-model="title" @keyup.enter="add" /> </div> </template> <script> // ID 生成器库 import { nanoid } from "nanoid"; export default { name: "UserHeader", data() { return { title: "", }; }, // 接收来自App的addTodo方法 // props: ["addTodo"], // 注意,这里不需要通过 props 传递数据,因此也就不需要 props 在子组件接收数据了 methods: { add() { // 校验数据 if (!this.title.trim()) return alert("输入不能为空"); // 将用户的输入包装成一个对象 const todoObj = { id: nanoid(), title: this.title, done: false }; console.log(todoObj) // 调用来自App的addTodo方法,通知App组件去添加一个对象 // this.addTodo(todoObj); // 触发UserHeader组件实例vc上的自定义事件addTodo,传递一个todoObj对象 this.$emit("addTodo", todoObj) // 清空输入 this.title = ""; }, }, }; </script> <style 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>
src/components/UserList.vue(无改动)
<template> <ul class="todo-main"> <!--根据todos数据,进行遍历--> <!--<UserItem--> <!-- v-for="todoObj in todos"--> <!-- :key="todoObj.id"--> <!-- :todo="todoObj"--> <!-- :checkTodo="checkTodo"--> <!-- :deleteTodo="deleteTodo"--> <!--></UserItem>--> <!--这里通过事件总线传递数据,因此就不通过props绑定"checkTodo"和"deleteTodo"--> <UserItem v-for="todoObj in todos" :key="todoObj.id" :todo="todoObj" ></UserItem> </ul> </template> <script> import UserItem from "./UserItem.vue"; export default { name: "UserList", components: { UserItem, }, // 接收来自App的数据 // props: ["todos", "checkTodo",'deleteTodo'], // 通过事件总线传递数据,因此也就不用 props 接收 "checkTodo"和"deleteTodo" props: ["todos"], }; </script> <style 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>
src/components/UserItem.vue( 这里负责提供数据,"checkTodo"仍然是用自定义事件的触发操作,但"deleteTodo" 使用发布消息实现)
<template> <li> <label> <input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/> <!--如下代码也能实现功能,但不推荐,违反修改props的原则--> <!--<input type="checkbox" v-model="todo.done" @change="handleCheck(todo.id)"/>--> <span>{{todo.title}}</span> </label> <button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button> </li> </template> <script> // 引入 pubsub-js库 import pubsub from "pubsub-js" export default { name: "UserItem", //声明接收对象 // props: ["todo", "checkTodo", "deleteTodo"], // 通过事件总线传递数据,因此也就不用 props 接收 "checkTodo"和"deleteTodo" props: ["todo"], // mounted() { // console.log(this.todo) // } methods: { //勾选or取消勾选 handleCheck(id) { console.log("勾选或取消勾选,id:",id) // this.checkTodo(id); // 提供数据,触发事件 this.$bus.$emit("checkTodo", id) }, //删除 handleDelete(id) { if (confirm("确定删除吗")) { console.log("删除,id:",id) // this.deleteTodo(id); // 提供数据,触发事件 // this.$bus.$emit("deleteTodo", id) // 提供消息(发布消息) pubsub.publish("deleteTodo", id) } }, }, }; </script> <style 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: #ddd; } li:hover button { display: block; } </style>
src/components/UserFooter.vue(无改动)
<template> <!-- v-show="total" 当total为0时,返回false,页面不显示,当total为非0时,返回true,页面显示--> <div class="todo-footer" v-show="total"> <label> <!--<input type="checkbox" :checked="isAll" @change="checkAll" />--> <!--为什么可以用v-model,这是因为绑定的是计算属性isAll,没有修改props的值--> <input type="checkbox" v-model="isAll"/> </label> <span> <span>已完成{{ doneTotal }}</span> / 全部{{ total }} </span> <button class="btn btn-danger" @click="clearAll">清除已完成任务</button> </div> </template> <script> // import { computed } from "vue"; export default { name: "UserFooter", // props: ["todos", "checkAllTodo", "clearAllTodo"], // 注意,这里不需要通过 props 传递"checkAllTodo", "clearAllTodo"数据,因此也就不需要 props 在子组件接收数据了 props: ["todos"], methods: { // checkAll(e) { // console.log("@checkAll",e.target.checked) // this.checkAllTodo(e.target.checked); // }, clearAll() { // this.clearAllTodo(); // 触发UserFooter组件实例vc上的自定义事件clearAllTodo this.$emit("checkAllTodo") }, }, computed: { total() { return this.todos.length; }, doneTotal() { // 1.遍历列表的方法 // let i = 0 // this.todos.forEach((todo)=>{ // if(todo.done === true) i++ // }); // return i // 2.reduce()方法,对数组中的每个元素按序执行一个提供的 reducer 函数,将其结果汇总为单个返回值 // const x = this.todos.reduce((pre, current) => { // console.log("@",pre) // return pre + (current.done ? 1 : 0); // }, 0); // return x; return this.todos.reduce( (pre, current) => pre + (current.done ? 1 : 0), 0 ); }, // isAll() { // // 判断已完成 / 全部 前面的复选框够不够,取决于已完成的个数是否等于全部的个数 // return this.doneTotal === this.total && this.total > 0; // }, // 计算属性 isAll 的完整写法 isAll:{ get(){ console.log("@isAll 的 get",this.doneTotal === this.total && this.total > 0) return this.doneTotal === this.total && this.total > 0; }, set(value){ console.log("@isAll 的 set",value) // this.checkAllTodo(value) // 触发UserFooter组件实例vc上的自定义事件checkAllTodo,传递一个value数据 this.$emit("checkAllTodo", value) } } }, }; </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>
src/App.vue(接收数据,在组件中仍然使用$bus绑定自定义事件和解绑事件"checkTodo",但"deleteTodo" 使用消息订阅和解除订阅实现)
<template> <div id="root"> <div class="todo-container"> <div class="todo-wrap"> <!--动态绑定函数,在父组件定义一个函数,子组件调用该函数,父组件就可以收到子组件传过来的参数 :addTodo="addTodo"--> <!--<UserHeader :addTodo="addTodo"></UserHeader>--> <!--使用自定义事件优化,通过父组件给子组件绑定一个自定义事件实现:子给父传递数据(第一种写法,使用v-on 或 @)--> <UserHeader @addTodo="addTodo"></UserHeader> <!--<UserList--> <!-- :todos="todos"--> <!-- :checkTodo="checkTodo"--> <!-- :deleteTodo="deleteTodo"--> <!--></UserList>--> <!--这里通过事件总线传递数据,因此就不通过props绑定"checkTodo"和"deleteTodo"--> <UserList :todos="todos"></UserList> <!--<UserFooter :todos="todos" :checkAllTodo="checkAllTodo" :clearAllTodo="clearAllTodo"></UserFooter>--> <!--使用自定义事件优化,通过父组件给子组件绑定一个自定义事件实现:子给父传递数据(第一种写法,使用v-on 或 @)--> <!--这里注意,:todos="todos" 动态绑定的是一个数组数据,而不是 子组件 ===> 父组件 所用的函数--> <UserFooter :todos="todos" @checkAllTodo="checkAllTodo" @clearAllTodo="clearAllTodo"></UserFooter> </div> </div> </div> </template> <script> // 引入 pubsub-js库 import pubsub from "pubsub-js" import UserHeader from "./components/UserHeader.vue"; import UserList from "./components/UserList.vue"; import UserFooter from "./components/UserFooter.vue"; export default { name: "App", components: { UserHeader, UserList, UserFooter, }, data() { return { // todos: [ // { id: "001", title: "起床", done: true }, // { id: "002", title: "洗漱", done: false }, // { id: "003", title: "睡觉", done: true }, // ], // 这里,将 todos 数据读取 localStorage 本地存储;如果初始化数据为空返回null时,则赋值一个空数组 // JSON.parse() 方法用来解析 JSON 字符串 // 在json中,||逻辑运算符 // 1.只要 || 前面为 false,不管 || 后面是 true 还是 false,都返回 || 后面的值。 // 2.只要 || 前面为 true,不管 || 后面是 true 还是 false,都返回 || 前面的值。 todos:JSON.parse(localStorage.getItem("todos")) || [] }; }, methods: { // 添加一个todo addTodo(todoObj) { console.log("我是APP组件,我收到了数据:",todoObj) // unshift() 在数组头部添加元素 this.todos.unshift(todoObj); }, // 勾选or取消勾选一个todo checkTodo(id) { // forEach() 方法用于调用数组的每个元素,并将元素传递给回调函数 this.todos.forEach((todo) => { // 将tudo的done属性进行取反 if (todo.id === id) todo.done = !todo.done; }); }, // 删除一个 // deleteTodo(id) { // // 数组.filter() 实现数组的过滤,创建一个新数组, 其包含通过所提供函数实现的测试的所有元素 // // 过滤出,todo.id 不是 id 的数据 // this.todos = this.todos.filter((todo) => todo.id !== id); // }, deleteTodo(_, id) { // 使用 _占位符代替msgName变量 // 数组.filter() 实现数组的过滤,创建一个新数组, 其包含通过所提供函数实现的测试的所有元素 // 过滤出,todo.id 不是 id 的数据 this.todos = this.todos.filter((todo) => todo.id !== id); }, // 全选or全不选 checkAllTodo(done) { // forEach() 方法用于调用数组的每个元素,并将元素传递给回调函数 this.todos.forEach((todo) => { todo.done = done; }); }, //清除所有已经完成的任务 clearAllTodo() { if(confirm("确认删除所有已完成的任务吗")){ // 数组.filter() 实现数组的过滤,创建一个新数组, 其包含通过所提供函数实现的测试的所有元素 // 过滤出,todo.done 还没完成 的数据 // this.todos = this.todos.filter((todo) => todo.done == false); this.todos = this.todos.filter((todo) => !todo.done); } }, }, // 使用监视属性,通过监视todos属性,实现 localStorage 本地存储 todos 数据 watch:{ todos:{ // 深度监视,监视对象内部值的改变 deep:true, handler(newValue){ // JSON.stringify() 方法将一个 JavaScript 对象或值转换为 JSON 字符串 console.log("@@监视属性",JSON.stringify(newValue)) localStorage.setItem("todos",JSON.stringify(newValue)) } } }, // 使用生命周期,实现在组件中传递数据(给$bus绑定和解绑事件) mounted() { // 接收数据,在组件中给$bus绑定自定义事件 this.$bus.$on("checkTodo", this.checkTodo) // this.$bus.$on("deleteTodo", this.deleteTodo) // 接收数据(消息订阅) this.pubId = pubsub.subscribe("deleteTodo", this.deleteTodo) }, beforeDestroy() { // 解绑事件 this.$bus.$off("checkTodo") // this.$bus.$off("deleteTodo") // 解除订阅 pubsub.unsubscribe(this.pubId) } }; </script> <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; margin: 0 auto; } .todo-container .todo-wrap { padding: 10px; border: 1px solid #ddd; border-radius: 5px; } </style>
src/main.js
import Vue from "vue"
import App from "./App.vue"
// 阻止 vue 在启动时生成生产提示
Vue.config.productionTip = false
new Vue({
el:"#app",
render:h => h(App),
// 安装全局事件总线
beforeCreate() {
Vue.prototype.$bus = this
}
})