手撕发布订阅模式 eventBus

什么是发布订阅模式

比如我们很喜欢看某个公众号号的文章,但是我们不知道什么时候发布新文章,要不定时的去翻阅;这时候,我们可以关注该公众号,当有文章推送时,会有消息及时通知我们文章更新了。

上面一个看似简单的操作,其实是一个典型的发布订阅模式,公众号属于发布者,用户属于订阅者;用户将订阅公众号的事件注册到调度中心,公众号作为发布者,当有新文章发布时,公众号发布该事件到调度中心,调度中心会及时发消息告知用户。

1-1. eventBus的基本使用

我们要实现eventBus,首先肯定要知道eventBus它目前具有哪些api

注册EventBus

//main.js
import Vue from 'vue';
...
Vue.prototype.EventBus = new Vue();

vm.$on( event, callback )

用法:监听当前实例上的自定义事件。事件可以由 vm.$emit 触发。回调函数会接收所有传入事件触发函数的额外参数。

vm.$emit( eventName, […args] )

用法:触发当前实例上的事件。附加参数都会传给监听器回调。

vm.$once( event, callback )

用法:监听一个自定义事件,但是只触发一次。一旦触发之后,监听器就会被移除。

vm.$off( [event, callback] )

用法:

  • 移除自定义事件监听器。
  • 如果没有提供参数,则移除所有的事件监听器;
  • 如果只提供了事件,则移除该事件所有的监听器;
  • 如果同时提供了事件与回调,则只移除这个回调的监听器。

父组件

import Son from '../components/Son';
export default {
       components: {
           Son,
       },
       mounted () {
           //监听grandson事件
           this.EventBus.$on("grandson",this.handleMmsg);
       },  
       methods: {
           handleMmsg(msg) {
               ....
               //移除事件监听,监听以后马上又移除了,故Parent只监听一次
               this.EventBus.$off("grandson",this.handleMmsg);
           }
       },
}
</script>

子组件

<template>
    <div>
        <hr>
        我是子组件 
        <br>
        <Grandson></Grandson>
    </div>
</template>
 
<script>
    import Grandson from '../components/Grandson';
    export default {
        components: {
            Grandson,
        }, 
        mounted () {
            //监听事件
            this.EventBus.$on("grandson",(msg)=>{
                console.log(msg);
            });
        },     
    }
</script>

孙组件

<template>
    <div>
        <hr>
        我是孙子组件
        <br>
        <button @click="handleMsg">点击发出消息</button>
    </div>
</template>
 
<script>
    export default {
        methods: {
            handleMsg() {
                //发出grandson事件
                this.EventBus.$emit("grandson","我是组件GrandSon")
            }
        },
    }
</script>

ok,现在我们来实现吧!!


1-2.实现

实现思路

  • 创建一个对象
  • 在该对象上创建一个缓存列表(调度中心)
  • on 方法用来把函数 fn 都加到缓存列表中(订阅者注册事件到调度中心)
  • emit 方法取到 arguments 里第一个当做 event,根据 event 值去执行对应缓存列表中的函数(发布者发布事件到调度中心,调度中心处理代码)
  • off 方法可以根据 event 值取消订阅(取消订阅)
  • once 方法只监听一次,调用完毕后删除缓存函数(订阅一次)

注意,on和emit不要搞混了,on才是监听收集事件,emit是发送触发事件,不管有没有emit发布事件,只要有on注册了事件,就会往listeners添加收集(先监听,再发布,发布过程中发现已经监听了就会执行监听回调,因此回调会在emit中执行)。比如说子组件通过emit触发了getName事件,那么所有的监听了这个事件的组件都会执行on方法。

  class EventEmitter {
    constructor() {
      // 维护事件及监听者
      this.listeners = {}
    }
    
    /**
     * 注册事件监听者
     * @param {String} type 事件类型,例如上面写的grandson事件
     * @param {Function} cb 回调函数,例如上面写的on接收的grandson对应的函数
     * 例如: EventBus.$on("grandson",(msg)=>{console.log(msg);})
     */
    on(type, cb) {
      if (!this.listeners[type]) {
        this.listeners[type] = []
      }
      this.listeners[type].push(cb)
    }
    
    /**
     * 发布事件
     * @param {String} type 事件类型
     * @param  {...any} args 参数列表,把emit传递的参数赋给回调函数
     * 例如: EventBus.$emit("grandson","我是组件GrandSon")
     */
    emit(type, ...args) {
      if (this.listeners[type]) {// 只要emit一触发,那么所有监听这个事件的on都会接收到,然后执行
        this.listeners[type].forEach(cb => {
          cb(...args)
        })
      }
    }
    
    /**
     * 移除某个事件的一个监听者
     * @param {String} type 事件类型
     * @param {Function} cb 回调函数
     * 例如:EventBus.$off("grandson",this.handleMmsg);
     */
    off(type, cb) {
      if (this.listeners[type]) {
        const targetIndex = this.listeners[type].findIndex(item => item === cb)
        if (targetIndex !== -1) {
          this.listeners[type].splice(targetIndex, 1)
        }
        if (this.listeners[type].length === 0) {
          delete this.listeners[type]
        }
      }
    }
    
    /**
     * 移除某个事件的所有监听者
     * @param {String} type 事件类型
     */
    offAll(type) {
      if (this.listeners[type]) {
        delete this.listeners[type]
      }
    }
  }
  
  // 创建事件管理器实例
  const ee = new EventEmitter()
  // 注册一个chifan事件监听者
  ee.on('chifan', function () { console.log('吃饭了,我们走!') })
  // 发布事件chifan
  ee.emit('chifan')
  // 也可以emit传递参数
  ee.on('chifan', function (address, food) { console.log(`吃饭了,我们去${address}${food}`) })
  ee.emit('chifan', '三食堂', '铁板饭') // 此时会打印两条信息,因为前面注册了两个chifan事件的监听者

  // 测试移除事件监听
  const toBeRemovedListener = function () { console.log('我是一个可以被移除的监听者') }
  ee.on('testoff', toBeRemovedListener)
  ee.emit('testoff')
  ee.off('testoff', toBeRemovedListener)
  ee.emit('testoff') // 此时事件监听已经被移除,不会再有console.log打印出来了

  // 测试移除chifan的所有事件监听
  ee.offAll('chifan')
  console.log(ee) // 此时可以看到ee.listeners已经变成空对象了,再emit发送chifan事件也不会有反应了

特点

  • 发布订阅模式中,对于发布者Publisher和订阅者Subscriber没有特殊的约束,他们好似是匿名活动,借助事件调度中心提供的接口发布和订阅事件,互不了解对方是谁。

  • 松散耦合,灵活度高,常用作事件总线

  • 易理解,可类比于DOM事件中的dispatchEventaddEventListener

手撕发布订阅模式 eventBus_第1张图片

缺点:

当事件类型越来越多时,难以维护,需要考虑事件命名的规范,也要防范数据流混乱。

你可能感兴趣的:(业务,vue,面试,javascript,前端,vue.js)