Skip to content
大纲

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

我们去搜索 24 种基本的设计模式,会发现其中并没有发布订阅模式;刚开始发布订阅模式只是观察者模式的一个别称,但是经过时间的沉淀,他改进了观察者模式的缺点,渐渐地开始独立于观察者模式。

我们也来看一下它的一个定义:

发布订阅模式是基于一个事件(主题)通道,希望接收通知的对象 Subscriber 通过自定义事件订阅主题,被激活事件的对象 Publisher 通过发布主题事件的方式通知各个订阅该主题的 Subscriber 对象。

有三种角色:发布者(Publisher)、订阅者(Subscriber)、调度中心/事件中心

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

观察者模式发布订阅模式
2 个角色3 个角色
重点是被观察者重点是调度中心
  • 观察者模式把观察者对象维护在目标对象中的,需要发布消息时直接发消息给观察者。在观察者模式中,目标对象本身是知道观察者存在的。
  • 发布/订阅模式中,发布者并不维护订阅者,也不知道订阅者的存在,所以也不会直接通知订阅者,而是通知调度中心,由调度中心通知订阅者。

在代码中发现有 watchwatcherobserveobserverlistenlistenerdispatchtriggeremitoneventeventbusEventEmitter 这类单词出现的地方,很有可能是在使用观察者模式或发布订阅的思想。不妨点进它的源码实现看看其他 coder 在实现观察者模式或发布订阅时有哪些巧妙的细节。

观察者模式实现

js
// 发布者
class Publisher {
  constructor(name, context) {
    this.name = name
    this.context = context
  }
  publish(type, content) {
    this.context.publish(type, content)

    // 当发布者发布消息时,通过调度中心,通知所有订阅者
    this.context.notify(type)
  }
}

// 订阅者
class Subscriber {
  constructor(name, context) {
    this.name = name
    this.context = context
  }
  subscribe(type, cb) {
    this.context.subscribe(type, cb)
  }
}

// 发布订阅中心
class PubSub {
  constructor() {
    this.messages = {}
    this.listeners = {}
  }
  publish(type, content) {
    const existContent = this.messages[type]
    if (!existContent) {
      this.messages[type] = []
    }
    this.messages[type].push(content)
  }
  subscribe(type, cb) {
    const existListener = this.listeners[type]
    if (!existListener) {
      this.listeners[type] = []
    }
    this.listeners[type].push(cb)
  }
  notify(type) {
    const messages = this.messages[type]
    const subscribers = this.listeners[type] || []
    subscribers.forEach((cb) => cb(messages))
  }
}

// testing
const TYPE_MUSIC = 'music'
const TYPE_MOVIE = 'movie'
const TYPE_NOVEL = 'novel'

const pubsub = new PubSub()

const publisherA = new Publisher('publisherA', pubsub)
const publisherB = new Publisher('publisherB', pubsub)
const publisherC = new Publisher('publisherC', pubsub)

publisherA.publish(TYPE_MUSIC, 'we are young')
publisherA.publish(TYPE_MOVIE, 'the silicon valley')
publisherB.publish(TYPE_MUSIC, 'stronger')
publisherC.publish(TYPE_MOVIE, 'imitation game')

const subscriberA = new Subscriber('subscriberA', pubsub)
subscriberA.subscribe(TYPE_MUSIC, (res) => {
  console.log('subscriberA received', res)
})
const subscriberB = new Subscriber('subscriberB', pubsub)
subscriberB.subscribe(TYPE_NOVEL, (res) => {
  console.log('subscriberB received', res)
})
const subscriberC = new Subscriber('subscriberC', pubsub)
subscriberC.subscribe(TYPE_MOVIE, (res) => {
  console.log('subscriberC received', res)
})

pubsub.notify(TYPE_MUSIC)
pubsub.notify(TYPE_MOVIE)
pubsub.notify(TYPE_NOVEL)

实现一个发布订阅模式

实现一个发布订阅模式,包含以下方法

  • 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)
      • 返回的一个函数实例,通过修改该函数原型对象来实现的

扩展