I’ve created small library that I use for Vue 3 apps

// Factory: createEventBus
const createEventBus = function () {
  const state = {
    listeners: {}
  }

  const methods = {
    // addEventListener
    $on(eventType, callback) {
      if (!Array.isArray(state.listeners[eventType])) {
        state.listeners[eventType] = []
      }
      if (state.listeners[eventType].indexOf(callback) === -1)
        state.listeners[eventType].push(callback)
    },
    // removeEventListener
    $off(eventType, callback) {
      if (Array.isArray(state.listeners[eventType])) {
        const index = state.listeners[eventType].indexOf(callback)
        if (index !== -1) state.listeners[eventType].splice(index, 1)
      }
    },
    // dispatchEvent
    $emit(eventType, data) {
      if (Array.isArray(state.listeners[eventType]))
        state.listeners[eventType].forEach(cb => cb(data))
    },
    // reset listeners
    $destroy() {
      state.listeners = {}
    }
  }

  return methods
}

// Class: EventBus
const EventBus = function () {
  Object.assign(this, createEventBus())
}

export default createEventBus
export { EventBus }
createEventBus.js

How to use

Factory way

import createEventBus from './createEventBus'

// create the product (from Factory)
const eventBus = createEventBus()
export default eventBus
eventBus.js
import eventBus from './eventBus'

// # addEventListener
eventBus.$on('message', console.log.bind(console, 'message:'))

const temporaryEvent = console.log.bind(console, 'temporary:')
eventBus.$on('temporary', temporaryEvent)

// # removeEventListener
eventBus.$off('temporary', temporaryEvent)

// # dispatchEvent
eventBus.$emit('message', 'Hello world')
// -> message: Hello world
eventBus.$emit('message', { name: 'Anna', age: 21 })
// -> {name: 'Anna', age: 21}

// # destroy events
eventBus.$destroy()

eventBus.$emit('message', 'This should not log after destroy')
// ->           

Class way

import { EventBus } from './createEventBus'

// instantiate EventBus
const eventBus = new EventBus()
export default eventBus
eventBus.js
import { EventBus } from './createEventBus'
import eventBus from './eventBus'

// # Check instance
console.log(eventBus instanceof EventBus)
// -> true

// # addEventListener
eventBus.$on('message', console.log.bind(console, 'message:'))

const temporaryEvent = console.log.bind(console, 'temporary:')
eventBus.$on('temporary', temporaryEvent)

// # removeEventListener
eventBus.$off('temporary', temporaryEvent)

// # dispatchEvent
eventBus.$emit('message', 'Hello world')
// -> message: Hello world
eventBus.$emit('message', { name: 'Anna', age: 21 })
// -> {name: 'Anna', age: 21}


// # destroy events
eventBus.$destroy()

eventBus.$emit('message', 'This should not log after destroy')
// ->           

Vue Option API way

No, I don’t like over-hyped Composition API
<script>
import createEventBus from './createEventBus'

export default {
  // LifeCycle Hooks
  beforeCreate() {
    // create the product (from Factory)
    const eventBus = eventEventBus()
    const {
      $on: $$on,
      $off: $$off,
      $emit: $$emit // Vue has internal $emit, change to $$emit
    } = eventBus
    // assign to Proxy Object
    Object.assign(
      this,
      { eventBus },
      {
        $$on,
        $$off,
        $$emit
      }
    )
  },
  mounted() {
    this.intervalID = setInterval(() => {
      this.$$emit('tick', 'Tick Tock')
    }, 1e3)
  },
  beforeUnmount() {
    // destroy IntervalID 
    clearInterval(this.intervalID)
    // destroyes all eventBus event listeners
    this.eventBus.$destroy()   
  }
}
</script>
HelloWorld.vue
import { createApp } form 'vue'
import HelloWorld from './HelloWorld.vue'

const template = document.createElement('template')
const app = createApp(vueOjbject, props)
const proxy = app.mount(template) // Proxy Object

// attach addEventListener
proxy.$$on('tick', data => {
  console.log('on tick:', data)
})
// -> on tick: Tick Tock
// -> on tick: Tick Tock
// -> on tick: Tick Tock