Skip to content
大纲

手写事件总线 EventEmitter(发布订阅模式)

实现一个发布订阅模式拥有 on emit once off 方法

js
// 事件总线(发布订阅模式)

class EventEmitter {
  constructor() {
    this.events = {}
  }
  on(name, fn) {
    const fns = this.events[name] || []
    fns.push({
      name,
      fn
    })
    this.events[name] = fns

    return this
  }
  off(name, fn) {
    const fns = this.events[name] || []

    const liveFns = []
    for (const item of fns) {
      if (item.fn !== fn && item.fn._ !== fn) {
        liveFns.push(item.fn)
      }
    }
    this.events[name] = liveFns

    return this
  }
  emit(name, ...rest) {
    // 创建副本,如果回调函数内继续注册相同事件,会造成死循环
    const fns = [...(this.events[name] || [])]

    for (const item of fns) {
      item.fn(...rest)
    }

    return this
  }
  once(name, fn) {
    const self = this

    function listener() {
      self.off(name, fn)
      fn(...arguments)
    }
    listener._ = fn

    return this.on(name, listener)
  }
}

// testing
const eventBus = new EventEmitter()

const fn1 = (name, age) => {
  console.log(`${name}'s age is ${age}`)
}
const fn2 = (name, age) => {
  console.log(`hello, ${name}'s age is ${age}`)
}
eventBus.on('say', fn1)
eventBus.on('say', fn1)
eventBus.once('say', fn2)
eventBus.emit('say', 'jack', 20)

参见: tiny-emitter

js
// 源码 https://github.com/scottcorgan/tiny-emitter/blob/master/index.js

function E() {
  // Keep this empty so it's easier to inherit from
  // (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)
}

E.prototype = {
  on: function (name, callback, ctx) {
    var e = this.e || (this.e = {})

    ;(e[name] || (e[name] = [])).push({
      fn: callback,
      ctx: ctx
    })

    return this
  },

  once: function (name, callback, ctx) {
    var self = this
    function listener() {
      self.off(name, listener)
      callback.apply(ctx, arguments)
    }

    listener._ = callback
    return this.on(name, listener, ctx)
  },

  emit: function (name) {
    var data = [].slice.call(arguments, 1)
    var evtArr = ((this.e || (this.e = {}))[name] || []).slice()
    var i = 0
    var len = evtArr.length

    for (i; i < len; i++) {
      evtArr[i].fn.apply(evtArr[i].ctx, data)
    }

    return this
  },

  off: function (name, callback) {
    var e = this.e || (this.e = {})
    var evts = e[name]
    var liveEvents = []

    if (evts && callback) {
      for (var i = 0, len = evts.length; i < len; i++) {
        if (evts[i].fn !== callback && evts[i].fn._ !== callback)
          liveEvents.push(evts[i])
      }
    }

    // Remove event from queue to prevent memory leak
    // Suggested by https://github.com/lazd
    // Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910

    liveEvents.length ? (e[name] = liveEvents) : delete e[name]

    return this
  }
}

module.exports = E
module.exports.TinyEmitter = E

对比分析

mitt 和 tiny-emitter 的对比分析

  • 共同点
    • 都支持 on(type, handler)、off(type, [handler]) 和 emit(type, [evt]) 三个方法来注册、注销、派发事件
  • 不同点
    • emit
      • all 属性,可以拿到对应的事件类型和事件处理函数的映射对象,是一个 Map 不是 {}
      • 支持监听 '*' 事件,可以调用 emitter.all.clear() 清除所有事件
      • 返回的是一个对象,对象存在上面的属性
    • tiny-emitter
      • 支持链式调用, 通过 e 属性可以拿到所有事件(需要看代码才知道)
      • 多一个 once 方法,并且支持设置 this(指定上下文 ctx)
      • 返回的一个函数实例,通过修改该函数原型对象来实现的

观察者模式 vs 发布订阅模式

观察者模式发布订阅模式
2 个角色3 个角色
重点是被观察者重点是发布订阅中心

扩展