前端vue入门(纯代码)13

13.Vue的消息订阅与发布

  • 备注:全局事件总线用的更多些,消息订阅与发布只需了解即可。
  • 【注意点1】:由于“消息订阅与发布”可依赖的第三方库太多了,这里使用pubsub-js

问题:“全局事件总线”和“消息订阅与发布”都可以实现任意组件间通信,那用哪个好?

答案:推荐使用“全局事件总线”,因为它是vue提供的,完全使用的vue技术,而“消息订阅与发布”则是第三方。

  • 消息订阅与发布的作用:一种组件间通信的方式,适用于任意组件间通信【与数据总线的作用相似】。

图解消息发布与订阅

前端vue入门(纯代码)13_第1张图片

【举例】C组件想给A组件传递数据:

  • A组件是订阅者,A订阅一个名叫demo的消息,并设置一个test回调函数;C组件是发布者,C会发布一个名叫demo的消息,并将C组件里面的数据传递出去。如上图所示,一旦C发布了demo消息【附带传递的数据,‘888’】,就会让A组件收到订阅消息,并执行回调函数test,并且来接收C传递过来的数据。【注意:回调函数test收到的第一个参数是订阅的消息名,第二个参数才是收到的data】

Student组件给School组件传递数据的使用步骤:

  1. 安装pubsub:npm i pubsub-js(安装不成功、使用管理员权限运行)

  2. 引入pubsub-js库【该库安装在node_modiles文件中】: import pubsub from 'pubsub-js'(订阅和发布都要引入)

  3. 订阅者订阅: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]
        });
    },
    
  4. 发布者发布:Student.vue文件中

    methods: {
    SendStudentData(){
      // 发布订阅消息haha,并传递数据params 
      const params =[this.name,this.age] 
      pubsub.publish('haha',params)
    }
    },
    
  5. 订阅者取消订阅: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>
    

前端vue入门(纯代码)13_第2张图片

补充知识点:

注意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">
    
		<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" 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>

你可能感兴趣的:(Vue前端,前端,vue.js,javascript)