【前端面试小册】JS-第24节:发布订阅模式进阶,实战运用

一、概述

1.1 定义

发布/订阅模式:基于一个主题/事件通道,希望接收通知的对象(订阅者)通过自定义事件订阅主题,被激活事件的对象(发布者)通过发布主题事件的方式被通知。

1.2 作用

  1. 广泛应用于异步编程中(替代了传递回调函数)
  2. 对象之间松散耦合的编写代码

高级前端必须掌握的知识点之一

1.3 核心思想

graph TD
    A[发布者 Publisher] --> B[事件中心 Event Center]
    B --> C[订阅者1 Subscriber1]
    B --> D[订阅者2 Subscriber2]
    B --> E[订阅者3 Subscriber3]

关键点

  • 发布者和订阅者不直接通信
  • 通过事件中心进行通信
  • 实现完全解耦

二、基础实现

2.1 基本结构

// 发布订阅模式
class EventEmitter {
    constructor() {
        // 事件对象,存放订阅的名字和事件
        this.events = {};
    }
    
    // 订阅事件的方法
    on(eventName, callback) {
        if (!this.events[eventName]) {
            // 注意是数组,一个名字可以订阅多个事件函数
            this.events[eventName] = [callback];
        } else {
            // 存在则 push 到指定数组的尾部保存
            this.events[eventName].push(callback);
        }
    }
    
    // 触发事件的方法
    emit(eventName, ...args) {
        // 遍历执行所有订阅的事件
        if (this.events[eventName]) {
            this.events[eventName].forEach(cb => cb(...args));
        }
    }
    
    // 移除订阅事件
    removeListener(eventName, callback) {
        if (this.events[eventName]) {
            this.events[eventName] = this.events[eventName].filter(cb => cb !== callback);
        }
    }
    
    // 只执行一次订阅的事件,然后移除
    once(eventName, callback) {
        // 绑定的是 fn,执行的时候会触发 fn 函数
        let fn = () => {
            callback();  // fn 函数中调用原有的 callback
            this.removeListener(eventName, fn);  // 删除 fn,再次执行的时候之后执行一次
        };
        this.on(eventName, fn);
    }
}

2.2 测试用例

const event = new EventEmitter();

const callback = () => {
    console.log('愚公上岸说');
};

event.on('test', callback);
event.emit('test');  // 输出:愚公上岸说

三、完整实现(增强版)

3.1 完整功能实现

class EventEmitter {
    constructor() {
        // 事件对象,存放订阅的名字和事件
        this.events = {};
    }
    
    // 订阅事件
    on(eventName, callback) {
        // 参数验证
        if (typeof callback !== 'function') {
            throw new TypeError('callback must be a function');
        }
        
        if (!this.events[eventName]) {
            this.events[eventName] = [];
        }
        
        // 避免重复订阅
        if (!this.events[eventName].includes(callback)) {
            this.events[eventName].push(callback);
        }
        
        // 返回 this,支持链式调用
        return this;
    }
    
    // 触发事件
    emit(eventName, ...args) {
        if (this.events[eventName]) {
            // 创建副本,避免在执行过程中修改原数组
            const callbacks = [...this.events[eventName]];
            callbacks.forEach(cb => {
                try {
                    cb(...args);
                } catch (error) {
                    console.error(`Error in event handler for ${eventName}:`, error);
                }
            });
        }
        return this;
    }
    
    // 移除订阅事件
    removeListener(eventName, callback) {
        if (this.events[eventName]) {
            this.events[eventName] = this.events[eventName].filter(cb => cb !== callback);
            
            // 如果没有订阅者了,删除该事件
            if (this.events[eventName].length === 0) {
                delete this.events[eventName];
            }
        }
        return this;
    }
    
    // 移除所有订阅
    removeAllListeners(eventName) {
        if (eventName) {
            delete this.events[eventName];
        } else {
            this.events = {};
        }
        return this;
    }
    
    // 只执行一次订阅的事件,然后移除
    once(eventName, callback) {
        const fn = (...args) => {
            callback(...args);
            this.removeListener(eventName, fn);
        };
        this.on(eventName, fn);
        return this;
    }
    
    // 获取指定事件的订阅者数量
    listenerCount(eventName) {
        return this.events[eventName] ? this.events[eventName].length : 0;
    }
    
    // 获取所有事件名
    eventNames() {
        return Object.keys(this.events);
    }
}

3.2 使用示例

const event = new EventEmitter();

// 订阅事件
event.on('test', (data) => {
    console.log('订阅者1:', data);
});

event.on('test', (data) => {
    console.log('订阅者2:', data);
});

// 触发事件
event.emit('test', '愚公上岸说');
// 输出:
// 订阅者1: 愚公上岸说
// 订阅者2: 愚公上岸说

// 只执行一次
event.once('once', () => {
    console.log('只执行一次');
});

event.emit('once');  // 输出:只执行一次
event.emit('once');  // 无输出

// 移除订阅
const callback = () => console.log('会被移除');
event.on('remove', callback);
event.removeListener('remove', callback);
event.emit('remove');  // 无输出

四、发布订阅 vs 观察者模式

4.1 区别

说到发布订阅肯定少不了提一下观察者模式,因为当你写完发布订阅后,面试官往往会问你,二者区别。其实二者在核心思想、运作机制上没有本质的差别。

核心区别

特性 发布订阅模式 观察者模式
中间人 存在第三方(事件中心) 不存在第三方
通信方式 发布者无法直接感知订阅者 发布者能直接感知订阅者
耦合度 完全解耦 部分耦合

4.2 类比理解

发布订阅模式:有三方,不直接通知

  • 发布者将文件传到第三方平台,平台通知给订阅者
  • 就像:你发布视频到 B 站,B 站通知所有订阅你的用户

观察者模式:无三方,直接通知

  • 发布者拉一个微信群,直接将文件发给每一个人
  • 就像:你在微信群里直接 @所有人

4.3 代码对比

观察者模式

// 观察者模式:发布者直接管理订阅者
class Subject {
    constructor() {
        this.observers = [];
    }
    
    addObserver(observer) {
        this.observers.push(observer);
    }
    
    notify(data) {
        this.observers.forEach(observer => observer.update(data));
    }
}

class Observer {
    update(data) {
        console.log('收到通知:', data);
    }
}

// 使用
const subject = new Subject();
const observer = new Observer();
subject.addObserver(observer);
subject.notify('消息');  // 发布者直接通知观察者

发布订阅模式

// 发布订阅模式:通过事件中心通信
class EventEmitter {
    constructor() {
        this.events = {};
    }
    
    on(eventName, callback) {
        if (!this.events[eventName]) {
            this.events[eventName] = [];
        }
        this.events[eventName].push(callback);
    }
    
    emit(eventName, data) {
        if (this.events[eventName]) {
            this.events[eventName].forEach(cb => cb(data));
        }
    }
}

// 使用
const event = new EventEmitter();
event.on('message', (data) => {
    console.log('收到通知:', data);
});
event.emit('message', '消息');  // 通过事件中心通知

五、实际应用场景

5.1 Vue 的事件总线

// Vue 2.x 中使用事件总线
// main.js
Vue.prototype.$bus = new Vue();

// 组件 A:发布事件
this.$bus.$emit('user-login', userInfo);

// 组件 B:订阅事件
this.$bus.$on('user-login', (userInfo) => {
    console.log('用户登录:', userInfo);
});

5.2 Node.js 的 EventEmitter

const EventEmitter = require('events');

class MyEmitter extends EventEmitter {}

const myEmitter = new MyEmitter();

myEmitter.on('event', () => {
    console.log('事件触发');
});

myEmitter.emit('event');

5.3 模块间通信

// 模块 A
import eventBus from './eventBus';

eventBus.emit('data-updated', newData);

// 模块 B
import eventBus from './eventBus';

eventBus.on('data-updated', (data) => {
    updateUI(data);
});

5.4 解耦业务逻辑

// 用户登录后,需要更新多个模块
class UserService {
    login(user) {
        // 登录逻辑
        eventBus.emit('user-login', user);
    }
}

// 购物车模块
eventBus.on('user-login', (user) => {
    loadCart(user.id);
});

// 消息模块
eventBus.on('user-login', (user) => {
    loadMessages(user.id);
});

六、观察者模式解决了什么?

6.1 优点

减少模块间的耦合问题,有它在,即便是两个分离的、毫不相关的模块,也可以实现数据通信。

6.2 缺点

但是没有完全地解决耦合问题:被观察者必须去维护一套观察者的集合,这些观察者必须实现统一的方法供被观察者调用。

七、发布订阅解决了什么?

7.1 优点

发布者完全不用感知订阅者,事件的注册和触发都发生在独立于双方的第三方,实现了完全解耦。

7.2 应用场景

  1. 异步编程:替代回调函数
  2. 模块解耦:不同模块间通信
  3. 事件驱动:基于事件的架构设计

八、性能优化

8.1 内存泄漏防护

class EventEmitter {
    constructor() {
        this.events = {};
        this.maxListeners = 10;  // 默认最大监听器数量
    }
    
    on(eventName, callback) {
        if (!this.events[eventName]) {
            this.events[eventName] = [];
        }
        
        // 检查监听器数量
        if (this.events[eventName].length >= this.maxListeners) {
            console.warn(`MaxListenersExceededWarning: ${eventName} has ${this.events[eventName].length} listeners`);
        }
        
        this.events[eventName].push(callback);
        return this;
    }
    
    // 设置最大监听器数量
    setMaxListeners(n) {
        this.maxListeners = n;
        return this;
    }
}

8.2 批量操作

class EventEmitter {
    // 批量订阅
    onMany(events, callback) {
        events.forEach(event => {
            this.on(event, callback);
        });
        return this;
    }
    
    // 批量移除
    removeMany(events, callback) {
        events.forEach(event => {
            this.removeListener(event, callback);
        });
        return this;
    }
}

九、面试要点总结

核心知识点

  1. 发布订阅模式:通过事件中心实现发布者和订阅者的解耦
  2. 核心方法:on(订阅)、emit(发布)、removeListener(移除)、once(只执行一次)
  3. 与观察者模式的区别:是否有第三方事件中心
  4. 应用场景:异步编程、模块解耦、事件驱动

常见面试题

Q1: 发布订阅模式和观察者模式的区别?

答:

  • 发布订阅模式:存在第三方事件中心,发布者和订阅者完全解耦
  • 观察者模式:不存在第三方,发布者直接管理订阅者,部分耦合

Q2: 如何实现发布订阅模式?

答:使用对象存储事件名和回调函数数组,提供 on 方法订阅、emit 方法发布、removeListener 方法移除订阅。

Q3: 发布订阅模式解决了什么问题?

答:解决了模块间耦合问题,实现了完全解耦,让不同模块可以通过事件中心进行通信。

实战建议

  • ✅ 理解发布订阅的核心思想
  • ✅ 掌握基本实现方法
  • ✅ 注意内存泄漏问题(及时移除订阅)
  • ✅ 理解与观察者模式的区别
#前端面试##前端#
全部评论

相关推荐

点赞 评论 收藏
分享
评论
点赞
1
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务