Skip to content

事件总线 EventEmitter 实现

描述

事件总线(EventEmitter)是发布-订阅模式的实现,用于组件间通信。

核心原理

  1. 发布-订阅模式:发布者不直接调用订阅者,通过事件中心中转
  2. 事件命名空间:支持按事件名订阅
  3. once:一次性事件订阅

实现代码

js
class EventEmitter {
    constructor() {
        this.events = {};
        this.maxListeners = 10;
    }

    on(event, listener) {
        if (!this.events[event]) {
            this.events[event] = [];
        }
        this.events[event].push(listener);

        if (this.events[event].length > this.maxListeners) {
            console.warn(`Possible EventEmitter memory leak, ${this.events[event].length} listeners added.`);
        }

        return this;
    }

    once(event, listener) {
        const wrapper = (...args) => {
            listener.apply(this, args);
            this.off(event, wrapper);
        };
        return this.on(event, wrapper);
    }

    off(event, listenerToRemove) {
        if (!this.events[event]) {
            return this;
        }

        this.events[event] = this.events[event].filter((listener) => listener !== listenerToRemove);

        return this;
    }

    emit(event, ...args) {
        if (!this.events[event]) {
            return false;
        }

        this.events[event].forEach((listener) => {
            listener.apply(this, args);
        });

        return true;
    }

    removeAllListeners(event) {
        if (event) {
            delete this.events[event];
        } else {
            this.events = {};
        }
        return this;
    }

    listenerCount(event) {
        return this.events[event] ? this.events[event].length : 0;
    }

    listeners(event) {
        return this.events[event] ? [...this.events[event]] : [];
    }
}

使用示例

js
const emitter = new EventEmitter();

// 普通订阅
emitter.on("message", (data) => {
    console.log("收到消息:", data);
});

emitter.emit("message", "Hello"); // 收到消息: Hello

// 一次性订阅
emitter.once("connect", () => {
    console.log("连接成功!");
});

emitter.emit("connect"); // 连接成功!
emitter.emit("connect"); // 不输出

// 取消订阅
function handler(data) {
    console.log("handler:", data);
}

emitter.on("event", handler);
emitter.emit("event", "test"); // handler: test
emitter.off("event", handler);
emitter.emit("event", "test"); // 不输出

// 获取监听器列表
emitter.on("event1", () => {});
emitter.on("event1", () => {});
emitter.on("event2", () => {});
console.log(emitter.listenerCount("event1")); // 2
console.log(emitter.listeners("event1")); // [函数, 函数]

实际应用

Vue EventBus(Vue 2)

js
// 创建全局事件总线
const EventBus = new EventEmitter();

// 组件 A:发送事件
EventBus.on('user-login', (user) => {
    console.log('用户登录:', user.name);
});

// 组件 B:触发事件
EventBus.emit('user-login', { name: '张三' });

// 组件卸载时清理
beforeUnmount() {
    EventBus.off('user-login');
}

事件委托

js
class DOMEventEmitter extends EventEmitter {
    constructor(element) {
        super();
        this.element = element;
        this.element.addEventListener = ((origFn) => {
            return (...args) => {
                origFn.apply(this.element, args);
            };
        })(this.element.addEventListener);
    }
}

Node.js EventEmitter 对比

方法说明Node.js
on添加监听器
once添加一次性监听器
off移除监听器✓(removeListener)
emit触发事件
removeAllListeners移除所有监听器
listenerCount监听器数量✓(listenerCount)
listeners获取监听器列表

面试追问点

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

特性发布-订阅观察者
通信方式通过事件中心直接通信
耦合度松耦合紧密耦合
复杂度较高(需事件中心)较低
灵活性

内存泄漏风险

js
// 未移除监听器可能导致内存泄漏
class Component {
    ngOnInit() {
        eventBus.on("data", this.handleData); // 未保存引用,无法移除
    }
}

// 正确做法
class Component {
    constructor() {
        this.boundHandler = this.handleData.bind(this);
    }

    ngOnInit() {
        eventBus.on("data", this.boundHandler);
    }

    ngOnDestroy() {
        eventBus.off("data", this.boundHandler);
    }
}