简单 通俗易懂,让你从逻辑分析开始慢慢手写一个简单的事件总线

139 阅读5分钟

代码就跟生活一样,今天我们聊聊事件总线

聊之前我们需要知道他是干嘛,为啥要有他.

事件总线:

我是一个通信手段,从前组件之间通信都需要靠面对面通信,一个传一个.

有了我不一样了.我跟电话微信一样,有事情直接电话联系呗.不用通过七大姑八大姨转达

应用场景:跨组件通信时,需要处理响应某件事情.

实际开发中,原来的业务场景是可以满足当时的业务需求的,但是后续新的场景添加进去,势必会出现跨组件通信的. 为了后续的可维护性,代码冗余性:

我们使用事件总线, 脱离组件.只针对某件事情 去做具体操作.让事件彼此通信,不依靠组件

如何实现事件总线这个功能呢?我们来分析一下他的逻辑

注释: 想直接看最终结果的 可以直接看--最终源码块.接下来的所有内容都是一些逻辑引导.方便小白理解,代码量很小

首先我们需要知道自己想要实现的功能

  1. 提供监听某个事件的接口,并告知监听到的后续操作

  2. 提供取消监听的接口

  3. 触发事件的接口(可传递数据)

  4. 触发事件后自动通知监听者

监听事件 on(监听事件名,监听到需要的操作)

取消监听  off(监听事件名,需要取消的操作)

通知监听者---触发监听事件 emit(要触发的事件,想要传递的消息---)

根据已知功能,设计一个数据结构

根据已有信息 我们现在需要设置一个简单的数据结构,然后通过操作数据结构,来得到我们想实现的目的

需要存放事件名,对应事件操作函数,自然而然我们想到了对象

设计结构初稿

const events = {
    '点击事件----类似电话号码': '点击事件对应的操作---我说话你不要插嘴但是要及时恢复',
    '滚动事件': '滚动事件的操作',
    '诸如此类': '诸如此类的操作'
}

然后我们存放的事件肯定不止一个,比如点击事件,不同dom,比如button的点击事件肯定是不一样的,所以我们需要存放多个事件

const evenBus = {
    '点击事件': [操作一,操作2],
    '滚动事件': [操作一,操作2],
    '诸如此类': [fn1,fn2,fn3]

有了数据结构,就跟你有了女神的联系方式一样,那我们就可以搞事情了

简单的思路

  • 就是监听到点击事件就往点击事件里面加一个操作

  • 点击时,遍历触发对应数组内所有的操作函数

  • 不想要这个操作时,在数组中删除掉该操作

那现在开始实现我们之前想要的功能了, 一个一个填充进去

监听女神的电话号码

//  数据结构
const Events = {}
//  功能:提供监听某个事件的接口,并告知监听到的后续操作
//  数据结构: 监听到对应事件,往数组中添加对应操作
//--函数结构 监听事件 on(监听事件名,监听到需要的操作)
function $on(eventName, handler) {
    //如果数据结构 不存在 eventName 这个事件 将其默认为一个数组或者Set 后续再把操作add进去 
    //为了避免重复这里用Set 可以理解为一个没有排序且自动去重的数组
    if (!Events[eventName]) {
        Events[eventName] = new Set()
    }
    Events[eventName].add(handler)
}
$on('女神的电话号码', '监听到女神来电后必须得震动加提醒呀')
$on('女神的电话号码', '打通后我要跟女神告白')
console.log('来看下执行$on后   数据结构是不是多了一个女神的电话', Events)一个女神的电话', Events)
    // * 提供取消监听的接口

    // * 触发事件的接口(可传递数据)

    // * 触发事件后自动通知监听者

接下来我们试试 取消监听后的对应操作

记得再上面代码的基础上往下写额 ,不要单独运行下面的代码. 因为我们定义的数据结构还在上面

//取消监听  off(监听事件名,需要取消的操作)
function $off(eventName,handler){
    if (!Events[eventName]) {
        //如果压根没监听这个事件----比如你好兄弟的号码
       console.log(eventName,'这谁呀,压根就没监听,不用理不用取消,压根没得取消 直接返回吧')
       return
    }
    Events[eventName].delete(handler)

}
思虑再三,告白还需谨慎,那么我们把告白操作删掉把
$off('女神的电话号码', '打通后我要跟女神告白')
console.log('看下告白操作是不是没了', Events)

还差一个触发事件对吧,来吧来吧

// * 触发事件的接口(可传递数据)
//  emit(要触发的事件,想要传递的消息---)
function $emit(eventName, ...args) {
    const handlers = Events[eventName]
    if (!handlers) return
    // 遍历执行数组内的所有内容 ..因为操作肯定是函数,所以前面的中文只是方便大家理解 后续改成对应操作函数额
    for (const handler of handlers) {
        handler(...args)
    }
}
我们试试这个$emit操作
function remind() {
    console.log('提醒你的女神来电了!')
}
//监听这个事件放入操作
$on('女神电话-111111', remind)
//触发这个事件,执行该事件的所有操作
$emit('女神电话-111111')
    // * 触发事件后自动通知监听者

完整实现代码 过滤引导

//  数据结构
const Events = {}
export default {
    $on(eventName, handler) {
        if (!Events[eventName]) {
            Events[eventName] = new Set()
        }
        Events[eventName].add(handler)
    },
    // * 提供取消监听的接口
    $off(eventName, handler) {
        if (!Events[eventName]) {
            return
        }
        Events[eventName].delete(handler)

    },
    // * 触发事件的接口(可传递数据)
    $emit(eventName, ...args) {
        const handlers = Events[eventName]
        if (!handlers) return
        for (const handler of handlers) {
            handler(...args)
        }
    }
}

以上内容正常情况小白应该也是完全没问题的,如果有问题赶紧打下基础或者针对不理解的地方搜索了解

当然在vue2中我们还有更快的实现方法

import Vue from 'vue';
export default new Vue({})

两行代码就搞定了 气不气.是不是觉得白学了

稳住,自己手写一遍底层实现能加升理解嘛,而且你应用的场景就没有限制了.

tip:

监听不用了:记得取消掉额,一直监听很呆的

讲到打电话,突然觉得蛮好玩的,给你做一个测试demo嘛,不考虑美观啊,就逻辑

demo 速写

为了节省时间,直接在Vue3官网中自带的单文件组件的环境中编写了,可以直接测试单文件组件

大概功能

  • 点击准备好啦----意味着开始监听--时刻看着女神的动态
  • 这个时候女神发动态就触发了 我们就能看到女神动态信息,及时回复了
  • 点击不准备时,意味着. 我压根没关注女神,他做什么我都不知道

image.png

<script setup>
import { ref, computed } from "vue";
import eventBus from "./eventBus.js";

const msg = ref("Hello World!");
const boyurl = ref(
  "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fhbimg.b0.upaiyun.com%2F6b7fca1df8bd2c134d0ab7050a740f08dccafb693044b7-thDTX8_fw658&refer=http%3A%2F%2Fhbimg.b0.upaiyun.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1666588736&t=60cf73007d72cc592eb308ccfe73721a"
);

const ready = ref(false);
const state = computed(() => {});
const call =(...msg)=>{
    alert('你的女神发动态啦:\n'+msg)
 }

const hander = function () {
  ready.value = !ready.value;
  if (!ready.value) {
    boyurl.value =
      "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fhbimg.b0.upaiyun.com%2F6b7fca1df8bd2c134d0ab7050a740f08dccafb693044b7-thDTX8_fw658&refer=http%3A%2F%2Fhbimg.b0.upaiyun.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1666588736&t=60cf73007d72cc592eb308ccfe73721a";
    eventBus.$off('fishing',call)
    
  } else {
    boyurl.value =
      "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg.soogif.com%2FW1RsUzGVmtjEoieJj4Oh29Rf4y3alG5C.gif_s400x0&refer=http%3A%2F%2Fimg.soogif.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1666589358&t=adebeabf43c7c617d89ddf057afff7d5";
    eventBus.$on('fishing',call)
  }
};
const share = function () {
 
  eventBus.$emit("fishing", "交个朋友?");
};

</script>

<template>
  <div class="container">
    <div class="boy">
      <img :src="boyurl" />
      <button class="callback">
        {{ ready ? "坐等女神动态......." : "出去玩喽,你找我....我也不知道" }}
      </button>
      <button class="state" @click="hander">
        {{ ready ? "不准备了" : "准备好啦" }}
      </button>
    </div>
    <div class="gril">
      <img src="" />
      <button @click="share">作为女神,我今天发一条朋友圈吧</button>
    </div>
  </div>
</template>
<style scoped>
.container {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}
.boy,
.gril {
  display: flex;
  align-items: center;
  justify-content: center;
}
button {
  margin: 20px;
  width: 300px;
  height: 50px;
}
.state {
  width: 100px;
}
img {
  width: 100px;
  height: 100px;

  object-fit: cover;
  border-radius: 50%;
}

进阶- 类的方式实现

vue3 中我们可以用混合类 来玩一下这个,妈妈再也不用担心我忘了取消监听了. 上面用的Set 下面用数组吧.都差不多 下面的代码我不说废话了,大家试着自己用类写一个

import { getCurrentInstance } from 'vue'

class EventBus {
  constructor(app) {
    if (!this.handles) {
      Object.defineProperty(this, 'handles', {
        value: {},
        enumerable: false
      })
    }
    this.app = app
    // _uid和EventName的映射
    this.eventMapUid = {}
  }
  setEventMapUid(uid, eventName) {
    if (!this.eventMapUid[uid]) {
      this.eventMapUid[uid] = []
    }
    this.eventMapUid[uid].push(eventName)
    // 把每个_uid订阅的事件名字push到各自uid所属的数组里
  }
  $on(eventName, callback, vm) {
    // vm是在组件内部使用时组件当前的this用于取_uid
    if (!this.handles[eventName]) {
      this.handles[eventName] = []
    }
    this.handles[eventName].push(callback)
    this.setEventMapUid(vm._uid, eventName)
  }
  $emit() {
    let args = [...arguments]
    let eventName = args[0]
    let params = args.slice(1)
    if (this.handles[eventName]) {
      let len = this.handles[eventName].length
      for (let i = 0; i < len; i++) {
        this.handles[eventName][i](...params)
      }
    }
  }
  $offVmEvent(uid) {
    let currentEvents = this.eventMapUid[uid] || []
    currentEvents.forEach(event => {
      this.$off(event)
    })
  }
  $off(eventName) {
    delete this.handles[eventName]
  }
}

let $EventBus = {}
$EventBus.install = (app) => {
  app.config.globalProperties.$eventBus = new EventBus(app)
  app.mixin({
    beforeUnmount() {
      const currentInstance = getCurrentInstance();
      // 拦截beforeUnmount钩子,自动销毁自身所有订阅的事件
      this.$eventBus.$offVmEvent(currentInstance._uid)
    }
  })
}
export default $EventBus