Search code examples
javascriptarrayspusheventemitter

Why is this simple array based event emitter order sensitive?


Simple event emitter does not fire an event handler if it is preceded by a '.once' event handler. Cannot understand why this happens.

const emitter = (host = {}, listeners = {}) => Object.assign(host, {
  emit (event, data) {
    (listeners[event] || []).forEach(h => h(data))
  },
  on (event, handler) {
    if (!listeners[event]) listeners[event] = []
    listeners[event].push(handler)
    return () => host.off(event, handler)
  },
  once (event, handler) {
    if (!listeners[event]) listeners[event] = []
    listeners[event].push(function h () {
      handler(...arguments)
      host.off(event, h)
    })
  },
  off (event, handler) {
    const i = (listeners[event] || []).findIndex(h => h === handler)
    if (i > -1) {
      listeners[event].splice(i, 1)
      if (!listeners[event].length) delete listeners[event]
    }
  }
})

// EXAMPLE

const e = emitter()

e.once('msg', msg => {
    console.log('once.msg: ', msg)
})

e.on('msg', msg => { // <- not firing
    console.log('on.msg: ', msg)
})

e.on('msg', msg => {
    console.log('on_1.msg: ', msg)
})

e.emit('msg' ,'See me?')

The first 'on.msg' handler does not fire at all, however if the 'once.msg' handler is moved below the other two everything fires smoothly, why is this?


Solution

  • That is because (listeners[event] || []).forEach(h => h(data)) will use an indexer starting at 0 and incrementing it for each item. When you delete an item (when calling once which calls off) the indexing is wrong and will actually skip the next item in line.

    Easiest way to remedy this is to make a copy of the events:

    (listeners[event] || []).slice().forEach(h => h(data));