面试常见题 - 如何实现发布订阅功能?

133 阅读4分钟

场景

在日常的前端开发中,经常会遇到跨组件间的通信问题。在vue2中,提供了下面的语法来支持(在vue3中已被移除)

  • $on:注册监听
  • $off:销毁监听
  • $once:只监听一次
  • $emit:触发事件

vue3中你可以使用vueuse库提供的useEventBus来实现同样的效果。

同时在nodejs中,也提供了events模块,方便开发者进行异步通信。

它是发布订阅模式的一种实现,如果你对观察者模式感兴趣,大橙子推荐你可以读一读这篇文章:观察者模式,同样这也是面试题中的高频考题😄

实现

准备工作

接下来基于ES6笔者带你一步步来实现这个功能:

先创建一个EventEmitter类:

class EventEmitter {
    constructor() {
        // ①
        this.events = new Map();
    }
}

在构造函数①中,新建一个Map类型数据来管理所有的发布订阅

实现增加监听(on)功能:

class EventEmitter {
    constructor() {
        this.events = new Map();
    }
    // ①
    on(type, handler) {
        // ②
        const handlers = this.events.get(type);

        if (handlers) {
            // ③
            handlers.push(handler);
        } else {
            // ④
            this.events.set(type, [handler]);
        }
    }
}

①:on需要传入两个参数:

  • type: 事件类型
  • handler: 事件触发时,需要执行的监听函数

②:在当前存储所有发布订阅的events中获取之前是否存储过该事件类型(type

有可能有多个地方同时订阅了一个事件,因此存储某个事件类型(type)监听函数的是一个数组。

当之前已经注册了该事件类型时,③将新的监听方法加入到数组中。

如果之前并未注册过该事件类型,④将新的事件类型及触发后监听的方法,存入events中。

实现销毁监听(off)功能

class EventEmitter {
    constructor() {
        this.events = new Map();
    }

    on(type, handler) {
        const handlers = this.events.get(type);

        if (handlers) {
            handlers.push(handler);
        } else {
            this.events.set(type, [handler]);
        }
    }

    // ①
    off(type, handler) {
        // ②
        const handlers = this.events.get(type);

        if (handlers) {
            // ③
            handlers.splice(handlers.indexOf(handler) >>> 0, 1);
        } else {
            // ④
            this.events.set(type, []);
        }
    }
}

①:销毁监听(off)同样需要传入下面两个参数:

  • type: 需要销毁监听的事件类型
  • handler: 需要销毁的监听函数

②:在当前存储所有发布订阅的events中获取之前是否存储过该事件类型(type

在③中,如果在已有的监听列表中,找到了该监听方法,那么就把它删除掉,如果没有找到那么 -1 >>> 0将会返回一个最大的32位整数,它是数组的最大长度,因此它不会删除任何一个元素。

在④中,如果这个时间类型并不存在,那么我们将会默认设置一个时间类型,监听函数的队列为空

实现触发事件(emit)功能

class EventEmitter {
    constructor() {
        this.events = new Map();
    }

    on(type, handler) {
        const handlers = this.events.get(type);

        if (handlers) {
            handlers.push(handler);
        } else {
            this.events.set(type, [handler]);
        }
    }

    off(type, handler) {
        const handlers = this.events.get(type);

        if (handlers) {
            handlers.splice(handlers.indexOf(handler) >>> 0, 1);
        } else {
            this.events.set(type, []);
        }
    }

    // ①
    emit(type, ...args) {
        // ②
        const handlers = this.events.get(type);

        if (handlers) {
            // ③
            handlers.slice().map(handler => { handler(...args) });
        }
    }
}

①:触发事件(emit)首先需要传入的参数是具体触发的事件类型(type),以及具体需要传递给监听参数的数据,由于可能有多个参数,因此使用...args

②:在当前存储所有发布订阅的events中获取之前是否存储过该事件类型(type

③:如果有就浅拷贝当前的监听数组,并且循环执行的同时传入...args

实现只监听一次(once)功能

class EventEmitter {
    constructor() {
        this.events = new Map();
    }

    on(type, handler) {
        const handlers = this.events.get(type);

        if (handlers) {
            handlers.push(handler);
        } else {
            this.events.set(type, [handler]);
        }
    }

    off(type, handler) {
        const handlers = this.events.get(type);

        if (handlers) {
            handlers.splice(handlers.indexOf(handler) >>> 0, 1);
        } else {
            this.events.set(type, []);
        }
    }

    emit(type, ...args) {
        const handlers = this.events.get(type);

        if (handlers) {
            handlers.slice().map(handler => { handler(...args) });
        }
    }

    // ①
    once(type, handler) {
        // ②
        const wrapper = (...args) => {
            handler(...args);
            this.off(type, wrapper);
        }

        // ③
        this.on(type, wrapper);
    }
}

①:实现只监听一次(once)需要传入两个参数:

  • type: 事件类型
  • handler: 只执行一次的监听函数

在②中基于传入的handler,增加执行完成后,自动执行销毁监听的方法

并在③中将新的方法添加到该事件类型的监听列表中

测试

接下来测试一下刚刚写好的EventEmitter

class EventEmitter {
    constructor() {
        this.events = new Map();
    }

    on(type, handler) {
        const handlers = this.events.get(type);

        if (handlers) {
            handlers.push(handler);
        } else {
            this.events.set(type, [handler]);
        }
    }

    off(type, handler) {
        const handlers = this.events.get(type);

        if (handlers) {
            handlers.splice(handlers.indexOf(handler) >>> 0, 1);
        } else {
            this.events.set(type, []);
        }
    }

    emit(type, ...args) {
        const handlers = this.events.get(type);

        if (handlers) {
            handlers.slice().map(handler => { handler(...args) });
        }
    }

    once(type, handler) {
        const wrapper = (...args) => {
            handler(...args);
            this.off(type, wrapper);
        }

        this.on(type, wrapper);
    }
}

const handleAlarm = (text) => {console.log(`闹钟响了!!现在是北京时间${text}`)}
const handleOnceAlarm = (text) => {console.log(`闹钟响了!!该闹钟只响一次!!现在是北京时间${text}`)}
const handleNeedDestroyAlarm = (text) => {console.log(`闹钟响了!!该闹钟响两次后会被销毁!!现在是北京时间${text}`)}
const eventEmitter = new EventEmitter();

eventEmitter.on('alarm', handleAlarm);
eventEmitter.on('alarm', handleNeedDestroyAlarm);
eventEmitter.once('alarm', handleOnceAlarm);
console.log('开始第一次响闹钟了!!');
eventEmitter.emit('alarm', new Date().toLocaleString());
console.log('开始第二次响闹钟了!!');
eventEmitter.emit('alarm', new Date().toLocaleString());
eventEmitter.off('alarm', handleNeedDestroyAlarm);
console.log('开始第三次响闹钟了!!');
eventEmitter.emit('alarm', new Date().toLocaleString());

执行后,你会得到下面的内容:

image.png

恭喜你,你已经成功实现了一个发布订阅功能!

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 10 天,点击查看活动详情