面试官:手写一个“发布-订阅模式”

发布-订阅模式又被称为观察者模式,它是定义在对象之间一对多的关系中,当一个对象发生变化,其他依赖于它的对象收到通知。在javascript的开发中,我们一般用事件模型替代发布-订阅模式。

DOM事件

javascript 复制代码
document.body.addEventListener('click',function(){

    alert(绑定1);

},false);

document.body.click();  //模拟点击
 
document.body.addEventListener('click',function(){

    alert(绑定2);

},false);
 
document.body.addEventListener('click',function(){

    alert(绑定3);
 
},false);
 
document.body.click();  //模拟点击

我们可以增加更多订阅者,不会对发布者的代码造成影响。注意,标准浏览器下用dispatchEvent实现。

自定义事件

① 确定发布者。(例如售票处)

② 添加缓存列表,便于通知订阅者。(预订车票列表)

③ 发布消息。遍历缓存列表。依次触发里面存放的订阅者回调函数(遍历列表,逐个发送短信)。

另外,我们还可以在回调函数填入一些参数,例如车票的价格之类信息。

javascript 复制代码
let ticketOffice = {};    //售票处

ticketOffice.clientList = [];    //缓存列表,存放订阅者的回调函数

ticketOffice.on = function (fn) {    //增加订阅者
    this.clientList.push(fn);    //订阅的消息添加进缓存列表
};

ticketOffice.emit = function () {    //发布消息
    for(var i = 0, fn; fn = this.clientList[i++];){
        fn.apply(this, arguments);    //arguments 是发布消息时带上的参数
    }
}

// 下面进行简单测试:

ticketOffice.on(function(time, path){    //小刚订阅消息
    console.log('时间:' + time);
    console.log('路线:' + path);
});


ticketOffice.on(function(time, path){    //小强订阅消息
    console.log('时间:' + time);
    console.log('路线:' + path);
});

ticketOffice.emit('晚上8:00','深圳-上海');
ticketOffice.emit('晚上8:10','上海-深圳');

至此,我们实现了一个最简单发布-订阅模式。不过这里存在一些问题,我们运行代码可以看到订阅者接收到了所有发布的消息。

javascript 复制代码
// 时间:晚上8:00
// 路线:深圳-上海
// 时间:晚上8:00
// 路线:深圳-上海
// 时间:晚上8:10
// 路线:上海-深圳
// 时间:晚上8:10
// 路线:上海-深圳

我们有必要加个key让订阅者只订阅 自己感兴趣的消息。改写后的代码如下:

javascript 复制代码
let ticketOffice = {};    //售票处

ticketOffice.clientList = [];    //缓存列表,存放订阅者的回调函数

ticketOffice.on = function (key, fn) {    //增加订阅者
    if (!this.clientList[key]){
        this.clientList[key] = [];
    }
    this.clientList[key].push(fn);    //订阅的消息添加进缓存列表
};

ticketOffice.emit = function () {    //发布消息
    let key = Array.prototype.shift.call(arguments),    //取出消息类型
        fns = this.clientList[key];    //取出该消息对应的回调函数集合
    if (!fns || fns.length === 0) {
        return false;
    }

    fns.forEach(fn => {
        fn.apply(this, arguments) //arguments 是发布消息时带上的参数
    })
}

// --------- 测试数据 --------------
ticketOffice.on('上海-深圳', function(time){    //小刚订阅消息
    console.log('小刚时间:' + time);
});

ticketOffice.on('深圳-上海', function(time){    //小强订阅消息
    console.log('小强时间:' + time);
});

ticketOffice.emit('深圳-上海', '晚上8:00');
ticketOffice.emit('上海-深圳', '晚上8:10');

// 小强时间:晚上8:00
// 小刚时间:晚上8:10

这样子,订阅者就可以只订阅自己感兴趣的事件了。

小强临时行程有变,不想订阅对应的消息了,我们还需要再新增一个移除订阅的方法

javascript 复制代码
let ticketOffice = {};    //售票处

ticketOffice.clientList = [];    //缓存列表,存放订阅者的回调函数

ticketOffice.on = function (key, fn) {    //增加订阅者
    if (!this.clientList[key]){
        this.clientList[key] = [];
    }
    this.clientList[key].push(fn);    //订阅的消息添加进缓存列表
};

ticketOffice.emit = function () {    //发布消息
    let key = Array.prototype.shift.call(arguments),    //取出消息类型
        fns = this.clientList[key];    //取出该消息对应的回调函数集合
    if (!fns || fns.length === 0) {
        return false;
    }

    fns.forEach(fn => {
        fn.apply(this, arguments) //arguments 是发布消息时带上的参数
    })
}

ticketOffice.remove = function (key, fn) {
    let fns = this.clientList[key]
    if (!fns) return false

    if (!fn) {
        fns && (fns.length = 0)
    } else {
        fns.forEach((cb, i) => {
            if (cb === fn) {
                fns.splice(i, 1)
            }
        })
    }
}

// --------- 测试数据 --------------
const xiaoGangOn = function(time){    //小刚订阅消息
    console.log('小刚时间:' + time);
}
const xiaoQiangOn = function(time){    //小强订阅消息
    console.log('小强时间:' + time);
}

ticketOffice.on('上海-深圳', xiaoGangOn);
ticketOffice.on('深圳-上海', xiaoQiangOn);

ticketOffice.remove('深圳-上海', xiaoQiangOn);

ticketOffice.emit('深圳-上海', '晚上8:00');
ticketOffice.emit('上海-深圳', '晚上8:10');

// 小刚时间:晚上8:10

至此,我们实现了一个相对完善的发布-订阅模式

但是可以看到使用数组进行时间的push和remove可能绑定相同的事件,且事件remove的效率低,我们可以用Set来替换Array

javascript 复制代码
let ticketOffice = {};    //售票处

ticketOffice.clientList = [];    //缓存列表,存放订阅者的回调函数

ticketOffice.on = function (key, fn) {    //增加订阅者
    if (!this.clientList[key]){
        this.clientList[key] = new Set();
    }
    this.clientList[key].add(fn);    //订阅的消息添加进缓存列表
};

ticketOffice.emit = function () {    //发布消息
    let key = Array.prototype.shift.call(arguments),    //取出消息类型
        fns = this.clientList[key];    //取出该消息对应的回调函数集合
    if (!fns || fns.length === 0) {
        return false;
    }

    fns.forEach(fn => {
        fn.apply(this, arguments) //arguments 是发布消息时带上的参数
    })
}

// 移除路线的单个订阅
ticketOffice.remove = function (key, fn) {
    this.clientList[key]?.delete(fn)
}
// 移除路线的所有订阅
ticketOffice.removeAll = function (key) {
    delete this.clientList[key]
}

// --------- 测试数据 --------------
const xiaoGangOn = function(time){    //小刚订阅消息
    console.log('小刚时间:' + time);
}
const xiaoQiangOn = function(time){    //小强订阅消息
    console.log('小强时间:' + time);
}

ticketOffice.on('上海-深圳', xiaoGangOn);
ticketOffice.on('深圳-上海', xiaoQiangOn);

ticketOffice.remove('深圳-上海', xiaoQiangOn);

ticketOffice.emit('深圳-上海', '晚上8:00');
ticketOffice.emit('上海-深圳', '晚上8:10');

// 小刚时间:晚上8:10

参考资料

《JavaScript 设计模式与开发实践》

相关推荐
子兮曰3 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰3 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万3 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝3 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋3 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁3 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
李少兄3 天前
JavaScript 隐式全局变量解析
javascript
汉堡大王95273 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端
梦想很大很大3 天前
从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践
前端·人工智能·后端
计算机魔术师3 天前
Meta Muse agent 接入 Shopify 的 Shop Pay 实现代理式购物
前端