Hone logo
Hone
Problems

Vue Event Bus Implementation Challenge

Many Vue applications grow to a point where direct prop drilling or complex state management becomes cumbersome. An event bus provides a simple and effective way for components to communicate with each other without direct coupling. This challenge asks you to implement a basic event bus system in TypeScript that can be used within a Vue application.

Problem Description

You need to create a TypeScript class that acts as a simple event bus. This event bus will allow different Vue components to emit events and listen for those events.

Key Requirements:

  1. on(event: string, callback: Function): This method should register a listener for a specific event. When the event is emitted, the provided callback function should be executed. A component can register multiple listeners for the same event.
  2. off(event: string, callback?: Function): This method should unregister a listener. If callback is provided, only that specific listener is removed. If callback is omitted, all listeners for that event should be removed.
  3. emit(event: string, ...args: any[]): This method should trigger an event. All registered listeners for the given event should be called with the provided ...args.

Expected Behavior:

  • When emit is called for an event, all functions registered with on for that event should be invoked.
  • The arguments passed to emit should be passed along to the callback functions.
  • off should correctly remove specific or all listeners for an event.
  • If emit is called for an event with no registered listeners, nothing should happen.

Edge Cases to Consider:

  • Emitting an event before any listeners are registered.
  • Calling off for an event that has no listeners.
  • Calling off with a callback that was never registered.
  • Registering the same callback multiple times for the same event.

Examples

Example 1: Basic Emission and Listening

Let's imagine two components: EmitterComponent and ListenerComponent.

// In EmitterComponent.vue (conceptual)
import eventBus from './eventBus'; // Assume eventBus is an instance of your class

export default {
  methods: {
    sendMessage() {
      eventBus.emit('user-message', 'Hello from Emitter!');
    }
  }
}

// In ListenerComponent.vue (conceptual)
import eventBus from './eventBus';

export default {
  mounted() {
    eventBus.on('user-message', this.handleMessage);
  },
  methods: {
    handleMessage(message: string) {
      console.log('Received message:', message); // Output: Received message: Hello from Emitter!
    }
  },
  beforeDestroy() {
    eventBus.off('user-message', this.handleMessage); // Crucial for cleanup
  }
}

Explanation: When sendMessage is called in EmitterComponent, it emits a user-message event with the payload "Hello from Emitter!". The ListenerComponent, which registered a listener for user-message in its mounted hook, will have its handleMessage method called with the provided message. The beforeDestroy hook ensures the listener is removed to prevent memory leaks.

Example 2: Multiple Listeners and Arguments

// In some ComponentA.vue
import eventBus from './eventBus';

export default {
  mounted() {
    eventBus.on('data-update', this.handleData1);
    eventBus.on('data-update', this.handleData2);
  },
  methods: {
    handleData1(id: number, value: string) {
      console.log(`Handler 1: ID=${id}, Value=${value}`);
    },
    handleData2(id: number, value: string) {
      console.log(`Handler 2: Received ID=${id} with value "${value}"`);
    }
  },
  beforeDestroy() {
    eventBus.off('data-update', this.handleData1);
    eventBus.off('data-update', this.handleData2);
  }
}

// In some ComponentB.vue
import eventBus from './eventBus';

export default {
  methods: {
    updateData() {
      eventBus.emit('data-update', 123, 'Important Info');
    }
  }
}

Expected Output (when ComponentB.updateData() is called):

Handler 1: ID=123, Value=Important Info
Handler 2: Received ID=123 with value "Important Info"

Explanation: When ComponentB emits data-update, both handleData1 and handleData2 registered by ComponentA are called with the provided arguments.

Example 3: Removing Specific and All Listeners

import eventBus from './eventBus';

// Assume some component registers these listeners
const handlerA = () => console.log('Handler A');
const handlerB = () => console.log('Handler B');
const handlerC = () => console.log('Handler C');

eventBus.on('test-event', handlerA);
eventBus.on('test-event', handlerB);
eventBus.on('test-event', handlerC);

// Removing a specific handler
eventBus.off('test-event', handlerB);
eventBus.emit('test-event'); // Expected: Handler A, Handler C

// Removing all handlers for an event
eventBus.off('test-event');
eventBus.emit('test-event'); // Expected: (nothing)

Explanation: Initially, all three handlers are registered. After calling off('test-event', handlerB), only handlerA and handlerC will be executed when test-event is emitted. Subsequently, calling off('test-event') removes all remaining listeners for that event, so the second emit call results in no output.

Constraints

  • The event bus implementation must be a TypeScript class.
  • Listeners should be functions.
  • Event names are strings.
  • The implementation should be efficient for a moderate number of events and listeners (e.g., hundreds or thousands, not millions).

Notes

  • Consider how you will store the listeners for each event. A Map or a plain object could be suitable.
  • Remember to handle the case where multiple listeners are registered for the same event.
  • Proper cleanup using off is crucial in Vue components to prevent memory leaks. Your implementation should facilitate this.
  • This is a foundational implementation. Advanced features like wildcard events or event priorities are out of scope for this challenge.
  • The goal is to build a robust and easy-to-use event bus, not necessarily the most performant one possible for extreme scenarios.
Loading editor...
typescript