Vue Custom Event Emitter
This challenge focuses on implementing a custom event emitter pattern within a Vue.js application using TypeScript. Understanding how to create and manage custom events is crucial for building flexible and decoupled Vue components that can communicate with each other without direct dependencies.
Problem Description
You are tasked with creating a reusable EventEmitter class that can be used throughout your Vue application to facilitate cross-component communication. This EventEmitter should allow components to:
- Register listeners for specific event names.
- Emit events with optional data.
- Remove listeners to prevent memory leaks.
You will then integrate this EventEmitter into a simple Vue component system to demonstrate its functionality.
Key Requirements:
EventEmitterClass:- A
listenersproperty (e.g., aMapor an object) to store event names and their corresponding callback functions. - A
on(eventName: string, callback: Function)method to register a listener. If the event already has listeners, the new callback should be added to the existing list. - A
emit(eventName: string, ...args: any[])method to trigger all callbacks registered for a giveneventName. The arguments passed toemitshould be forwarded to the callbacks. - A
off(eventName: string, callback?: Function)method to remove listeners. If nocallbackis provided, all listeners foreventNameshould be removed. If acallbackis provided, only that specific callback should be removed.
- A
- Vue Component Integration:
- Create a parent component (
App.vueor similar) that will manage an instance of theEventEmitter. - Create at least two child components (
ComponentA.vueandComponentB.vue). ComponentAshould be able to emit an event using the sharedEventEmitterinstance.ComponentBshould listen for this emitted event and update its own state or display a message based on the event data.
- Create a parent component (
Expected Behavior:
- When an instance of
EventEmitteris created, it should be empty of listeners. - When
onis called multiple times for the same event, all callbacks should be executed when the event is emitted. - When
emitis called with an event name that has registered listeners, all corresponding callbacks should be invoked with the provided arguments. - When
offis called without a callback, all listeners for that event should be cleared. - When
offis called with a specific callback, only that callback should be removed, and other callbacks for the same event should remain. - In the Vue integration,
ComponentAshould trigger an event (e.g.,dataUpdated) with some payload.ComponentBshould react to this event by displaying the payload or a derived message.
Edge Cases:
- Emitting an event for which no listeners are registered should not cause errors.
- Removing a listener that does not exist should not cause errors.
- Handling
nullorundefinedas arguments passed toemit.
Examples
Example 1: Basic EventEmitter Usage (Conceptual)
// Conceptual Example - Not directly Vue integration yet
const emitter = new EventEmitter();
const handler1 = (message: string) => {
console.log('Handler 1 received:', message);
};
const handler2 = (count: number) => {
console.log('Handler 2 received count:', count);
};
emitter.on('greeting', handler1);
emitter.on('greeting', (message: string) => {
console.log('Another greeting handler:', message.toUpperCase());
});
emitter.on('countUpdate', handler2);
// Output:
// Handler 1 received: Hello!
// Another greeting handler: HELLO!
// Handler 2 received count: 10
emitter.emit('greeting', 'Hello!');
emitter.emit('countUpdate', 10);
emitter.off('greeting', handler1);
// Output:
// Another greeting handler: GOODBYE!
emitter.emit('greeting', 'Goodbye!');
emitter.off('greeting'); // Removes all remaining 'greeting' listeners
emitter.emit('greeting', 'This will not be logged');
Example 2: Vue Component Integration Scenario
Imagine ComponentA fetches data and wants to inform other parts of the application. ComponentB needs to display this data.
App.vue:- Instantiates
EventEmitter. - Passes the
EventEmitterinstance down toComponentAandComponentB(e.g., via provide/inject or props).
- Instantiates
ComponentA.vue:- Has a button that, when clicked, fetches some dummy data (e.g., a user's name) and emits a
userLoggedInevent with the user's name as payload using the sharedEventEmitter.
- Has a button that, when clicked, fetches some dummy data (e.g., a user's name) and emits a
ComponentB.vue:- Registers a listener for the
userLoggedInevent on the sharedEventEmitter. - When the
userLoggedInevent is received, it updates its local state to store the user's name and displays a welcome message.
- Registers a listener for the
// --- App.vue ---
<template>
<div>
<ComponentA :emitter="eventBus" />
<ComponentB :emitter="eventBus" />
</div>
</template>
<script lang="ts">
import { defineComponent, provide } from 'vue';
import ComponentA from './components/ComponentA.vue';
import ComponentB from './components/ComponentB.vue';
import EventEmitter from './eventEmitter'; // Assuming EventEmitter is in ./eventEmitter.ts
export default defineComponent({
components: {
ComponentA,
ComponentB,
},
setup() {
const eventBus = new EventEmitter();
// Using provide for simplicity, props could also be used
provide('eventBus', eventBus);
return {
eventBus,
};
},
});
</script>
// --- ComponentA.vue ---
<template>
<div>
<button @click="loginUser">Login User</button>
</div>
</template>
<script lang="ts">
import { defineComponent, inject } from 'vue';
export default defineComponent({
setup() {
const eventBus = inject('eventBus');
const loginUser = () => {
const userName = 'Alice'; // Simulate fetching user data
if (eventBus) {
(eventBus as any).emit('userLoggedIn', userName);
}
};
return {
loginUser,
};
},
});
</script>
// --- ComponentB.vue ---
<template>
<div>
<p v-if="loggedInUser">Welcome, {{ loggedInUser }}!</p>
<p v-else>Please log in.</p>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, onMounted, onUnmounted, inject } from 'vue';
export default defineComponent({
setup() {
const loggedInUser = ref<string | null>(null);
const eventBus = inject('eventBus');
const handleUserLogin = (userName: string) => {
loggedInUser.value = userName;
};
onMounted(() => {
if (eventBus) {
(eventBus as any).on('userLoggedIn', handleUserLogin);
}
});
onUnmounted(() => {
if (eventBus) {
(eventBus as any).off('userLoggedIn', handleUserLogin);
}
});
return {
loggedInUser,
};
},
});
</script>
Example 3: Multiple Listeners and Removal
Demonstrates more complex listener management.
- Scenario: A "global notification" event is emitted. Multiple components might listen for it, each reacting differently. A specific component might later decide to stop listening.
// Imagine ComponentC and ComponentD also listen for 'globalNotification'
// ComponentC displays a simple toast.
// ComponentD increments a counter.
// When 'globalNotification' is emitted:
// - ComponentC's toast appears.
// - ComponentD's counter increases.
// If ComponentC is unmounted and cleans up its listener:
// - Emitting 'globalNotification' only affects ComponentD's counter.
Constraints
- The
EventEmitterclass must be implemented in TypeScript. - The Vue components should be set up using Vue 3's Composition API.
- The provided
EventEmitterinstance should be shared between components, and you should choose an appropriate mechanism for this (e.g.,provide/injector passing via props). - Memory leaks must be prevented by ensuring listeners are removed when components are unmounted (
onUnmountedhook). - Do not use any external event emitter libraries. Implement the core logic yourself.
Notes
- Consider using a
Map<string, Function[]>or a similar structure to store listeners, mapping event names to arrays of callback functions. - Ensure type safety where possible, especially when defining the
EventEmitterclass and its methods. - The
provide/injectmechanism is a good way to share theEventEmitterinstance across the component tree in Vue 3. - Think about how to gracefully handle situations where
eventBusmight not be injected (though in this controlled example, it should always be available). - Success will be measured by the correct implementation of the
EventEmitterclass and its seamless integration into the Vue components, resulting in reliable event propagation and cleanup.