手写JavaScript事件总线

1,598 阅读6分钟

需求分析

一提起事件总线我们首先想到的就是要有事件监听、触发事件等功能。大概包括如下几点

  1. on 事件监听
  2. emit 事件触发
  3. off 事件取消
  4. once 单次监听

编写代码

编写结构

首先创建一个名为 AriesEventBus 的类,在类里面定义事件监听、事件触发等方法,把功能结构先给写好

class AriesEventBus {
  constructor() {

  }

  // 事件监听
  on(eventName, eventCallback) {

  }

  // 事件触发
  emit(eventName, ...args) {

  }

  // 移除事件监听
  off(eventName, eventCallback) {

  }

  // 单次监听
  once(eventName, eventCallback) {
    
  }
}

实现 事件监听(on) 功能

如果有使用过事件总线的库的小伙伴应该知道一般情况下我们监听事件的格式会如下:

  • 第一次参数为要监听的事件名
  • 第二个参数为要执行的回调函数
eventBus.on("事件名", (...arg) => {

})

因此我们可以在代码中这样实现:

class AriesEventBus {
  constructor() {
      // 在类初始化的时候就往this中绑定一个eventBus,用于存储事件名和事件函数
    this.eventBus = {}
  }

  // 事件监听
  on(eventName, eventCallback, thisArg) {
      // 判断事件名是否是 string 类型
    if (typeof eventName !== "string") {
      throw TypeError("传入的事件名数据类型需为string类型")
    }
      // 判断事件函数是否是 function 类型
    if (typeof eventCallback !== "function") {
      throw TypeError("传入的回调函数数据类型需为function类型")
    }
	
      
      // 从 eventBus 中获取对应的事件数据
    let handlers = this.eventBus[eventName]
    // 如果 handlers 不存在那就代表从未在 eventBus 上挂载过该事件的 handlers
    if (!handlers) {
        // 给 handlers 赋值为数组类型,再将 eventName 挂载到 eventBus 上
      handlers = []
      this.eventBus[eventName] = handlers
    }

      // 在上一步的if语句中已经将引用数据类型的 handlers 赋值给了this.eventBus[eventName]
      // 这样我们在这一步进行 push 操作的时候也会修改 this.eventBus[eventName] 的数据
      // 因为 array 是引用类型,handlers 和 this.eventBus[eventName] 存放的都是一样内存地址
    handlers.push({
      eventCallback,
      thisArg
    })
  }
}

实现事件触发(emit)功能

一般情况下,我们要触发事件的话代码会如下格式:

  • 第一个参数为需要触发的事件名称
  • 之后的参数为args,也就是一个或多个传进事件监听函数的数据
eventBus.emit("事件名", args)

代码实现如下:

class AriesEventBus {
  constructor() {
    this.eventBus = {}
  }
    
  // 事件监听
  on(eventName, eventCallback, thisArg) {
      // 判断事件名是否是 string 类型
    if (typeof eventName !== "string") {
      throw TypeError("传入的事件名数据类型需为string类型")
    }
      // 判断事件函数是否是 function 类型
    if (typeof eventCallback !== "function") {
      throw TypeError("传入的回调函数数据类型需为function类型")
    }
	
      
      // 从 eventBus 中获取对应的事件数据
    let handlers = this.eventBus[eventName]
    // 如果 handlers 不存在那就代表从未在 eventBus 上挂载过该事件的 handlers
    if (!handlers) {
        // 给 handlers 赋值为数组类型,再将 eventName 挂载到 eventBus 上
      handlers = []
      this.eventBus[eventName] = handlers
    }

      // 在上一步的if语句中已经将引用数据类型的 handlers 赋值给了this.eventBus[eventName]
      // 这样我们在这一步进行 push 操作的时候也会修改 this.eventBus[eventName] 的数据
      // 因为 array 是引用类型,handlers 和 this.eventBus[eventName] 存放的都是一样内存地址
    handlers.push({
      eventCallback,
      thisArg
    })
  }

  // 事件触发
  emit(eventName, ...args) {
    // 判断事件名是否是 string 类型
    if (typeof eventName !== "string") {
      throw TypeError("传入的事件名数据类型需为string类型")
    }

    // 从 eventBus 中获取对应的事件数据
    const handlers = this.eventBus[eventName] || []
    
    // 遍历 handlers 调用 handlers 中的 eventCallback
    handlers.forEach((handler) => {
      handler.eventCallback.apply(handler.thisArg, args)
    })
  }
}

实现移除事件监听(off)

调用移出事件监听的写法跟添加事件监听一致,off(移出的事件名,移除的事件函数 ),有一点需要注意,我们一般移出一个事件并不是将所有跟这个事件绑定的 eventCallback 都给移除掉,二是移除对应的的事件函数,也就是 off(移出的事件名,移除的事件函数 ) 的第二个参数

代码如下:

class AriesEventBus {
  constructor() {
    this.eventBus = {}
  }
    
  // 事件监听
  on(eventName, eventCallback, thisArg) {
      // 判断事件名是否是 string 类型
    if (typeof eventName !== "string") {
      throw TypeError("传入的事件名数据类型需为string类型")
    }
      // 判断事件函数是否是 function 类型
    if (typeof eventCallback !== "function") {
      throw TypeError("传入的回调函数数据类型需为function类型")
    }
	
      
      // 从 eventBus 中获取对应的事件数据
    let handlers = this.eventBus[eventName]
    // 如果 handlers 不存在那就代表从未在 eventBus 上挂载过该事件的 handlers
    if (!handlers) {
        // 给 handlers 赋值为数组类型,再将 eventName 挂载到 eventBus 上
      handlers = []
      this.eventBus[eventName] = handlers
    }

      // 在上一步的if语句中已经将引用数据类型的 handlers 赋值给了this.eventBus[eventName]
      // 这样我们在这一步进行 push 操作的时候也会修改 this.eventBus[eventName] 的数据
      // 因为 array 是引用类型,handlers 和 this.eventBus[eventName] 存放的都是一样内存地址
    handlers.push({
      eventCallback,
      thisArg
    })
  }

  // 事件触发
  emit(eventName, ...args) {
    // 判断事件名是否是 string 类型
    if (typeof eventName !== "string") {
      throw TypeError("传入的事件名数据类型需为string类型")
    }

    // 从 eventBus 中获取对应的事件数据
    const handlers = this.eventBus[eventName]
    // 判断 handlers 是否存在,不过不存在那就证明 该触发的事件名并未挂载到 eventBus 上
    if (!handlers) {
      throw new Error("该触发的事件名并未挂载到 eventBus 上")
    }
    // 遍历 handlers 调用 handlers 中的 eventCallback
    handlers.forEach((handler) => {
      handler.eventCallback.apply(handler.thisArg, args)
    })
  }
    
  // 移除事件监听
  off(eventName, eventCallback) {
      // 判断事件名是否是 string 类型
    if (typeof eventName !== "string") {
      throw TypeError("传入的事件名数据类型需为string类型")
    }
      // 判断事件函数是否是 function 类型
    if (typeof eventCallback !== "function") {
      throw TypeError("传入的回调函数数据类型需为function类型")
    }
	
    const handlers = this.eventBus[eventName] || []
    // 如果 eventName 在 eventBus 中存在则进行操作
    if (handlers.length) {
      const newHandlers = [...handlers]
      for (let i = 0; i < newHandlers.length; i++) {
        const handler = newHandlers[i]
        if (handler.eventCallback === eventCallback) {
          const index = handlers.indexOf(handler)
          handlers.splice(index, 1)
        }
      }
    }
      
    // 判断 handlers 的 length 是否为 0,若为 0 那则代表 eventBus中已经没有挂载该事件,可直接删除
    if (handlers.length === 0) {
      delete this.eventBus[eventName]
    }
    
  }
}

实现单次监听(once)

顾名思义,也就是对事件只监听一次,写法格式:

eventBus.once(事件名,事件函数)

代码如下:

class AriesEventBus {
  constructor() {
    this.eventBus = {}
  }
    
  // 事件监听
  on(eventName, eventCallback, thisArg) {
      // 判断事件名是否是 string 类型
    if (typeof eventName !== "string") {
      throw TypeError("传入的事件名数据类型需为string类型")
    }
      // 判断事件函数是否是 function 类型
    if (typeof eventCallback !== "function") {
      throw TypeError("传入的回调函数数据类型需为function类型")
    }
	
      
      // 从 eventBus 中获取对应的事件数据
    let handlers = this.eventBus[eventName]
    // 如果 handlers 不存在那就代表从未在 eventBus 上挂载过该事件的 handlers
    if (!handlers) {
        // 给 handlers 赋值为数组类型,再将 eventName 挂载到 eventBus 上
      handlers = []
      this.eventBus[eventName] = handlers
    }

      // 在上一步的if语句中已经将引用数据类型的 handlers 赋值给了this.eventBus[eventName]
      // 这样我们在这一步进行 push 操作的时候也会修改 this.eventBus[eventName] 的数据
      // 因为 array 是引用类型,handlers 和 this.eventBus[eventName] 存放的都是一样内存地址
    handlers.push({
      eventCallback,
      thisArg
    })
  }

  // 事件触发
  emit(eventName, ...args) {
    // 判断事件名是否是 string 类型
    if (typeof eventName !== "string") {
      throw TypeError("传入的事件名数据类型需为string类型")
    }

    // 从 eventBus 中获取对应的事件数据
    const handlers = this.eventBus[eventName]
    // 判断 handlers 是否存在,不过不存在那就证明 该触发的事件名并未挂载到 eventBus 上
    if (!handlers) {
      throw new Error("该触发的事件名并未挂载到 eventBus 上")
    }
    // 遍历 handlers 调用 handlers 中的 eventCallback
    handlers.forEach((handler) => {
      handler.eventCallback.apply(handler.thisArg, args)
    })
  }
    
  // 移除事件监听
  off(eventName, eventCallback) {
      // 判断事件名是否是 string 类型
    if (typeof eventName !== "string") {
      throw TypeError("传入的事件名数据类型需为string类型")
    }
      // 判断事件函数是否是 function 类型
    if (typeof eventCallback !== "function") {
      throw TypeError("传入的回调函数数据类型需为function类型")
    }
	
    const handlers = this.eventBus[eventName] || []
    // 如果 eventName 在 eventBus 中存在则进行操作
    if (handlers.length) {
      const newHandlers = [...handlers]
      for (let i = 0; i < newHandlers.length; i++) {
        const handler = newHandlers[i]
        if (handler.eventCallback === eventCallback) {
          const index = handlers.indexOf(handler)
          handlers.splice(index, 1)
        }
      }
    }
      
    // 判断 handlers 的 length 是否为 0,若为 0 那则代表 eventBus中已经没有挂载该事件,可直接删除
    if (handlers.length === 0) {
      delete this.eventBus[eventName]
    }
    
  }
    
   // 单次监听
  once(eventName, eventCallback, thisArg) {
    // 判断事件名是否是 string 类型
    if (typeof eventName !== "string") {
      throw TypeError("传入的事件名数据类型需为string类型")
    }
    // 判断事件函数是否是 function 类型
    if (typeof eventCallback !== "function") {
      throw TypeError("传入的回调函数数据类型需为function类型")
    }
    
      // 在内部创建一个函数,将这个函数绑定到属性名为eventName的eventBus上
    const tempCallback = (...args) => {
      // 当第一次触发事件后在执行这一步的时候就通过 off 来移除这个事件函数, 这样这个函数只会执行一次
      this.off(eventName, tempCallback)
      eventCallback.apply(thisArg, args)
    }

    this.on(eventName, tempCallback)
  }
}

测试

至此我们所需要实现的功能都已经写好了,接下来我们来测试一下

const eventBus = new AriesEventBus()

function nameCallback(...args) {
  console.log("name发生改变了, 值为:" + args);
} 
function name2Callback(...args) {
  console.log("name2发生改变了, 值为:" + args);
} 
function name3Callback(...args) {
  console.log("name3发生了改变:" + args);
}
eventBus.on("name", nameCallback)
eventBus.on("name", nameCallback)
eventBus.on("name", name2Callback)
eventBus.once("name", name3Callback)

setTimeout(() => {
  console.log("触发事件");
  eventBus.emit("name", "why", "zjl")
}, 1000);
setTimeout(() => {
  console.log("移除事件");
  eventBus.off("name", nameCallback)
}, 2000);
setTimeout(() => {
  console.log("再次触发事件");
  eventBus.emit("name", "why", "哈哈哈哈")
}, 3000);

控制台输出结果:

image.png

总结

其实完成功能所需的代码并不复杂,称得上很简单,难的在于是否敢去尝试,这个世界上绝大多数事情还没到拼智商的程度,共勉之!!!