阅读 235

发布订阅模式

发布订阅是什么?

发布订阅模式又叫观察者模式,它定义对象间的一种 一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象豆浆得到通知。在javascript开发中,一般用事件模型来替代传统的发布-订阅模式。 通过事件监听的方式将需要异步执行的事件放入一个事件队列,等时机后统一执行

实现发布订阅通常的数据结构为

const eventLoop  = {
    key1:[fn1,fn2,fn3], // 事件key1,订阅额事件fn1,fn2.fn3先放入队列
    key2:[fn2,fn3,fn4], // 事件key2,订阅额事件fn2,fn3.fn4先放入队列
    ....
}
// 事件发布的事件,遍历eventLoop['event1']中的所有事件进行调用
复制代码

创建一个简单的发布订阅模式

  • 首先要指定好谁充当发布者(比如售楼处);
  • 然后给发布者添加一个缓存列表,用于存放回调函数以便通知订阅者(售楼处的花名册)
  • 最后发布消息的时候,发布者会遍历这个缓存列表,依次触发里面存放的订阅者回调函数(遍历整个花名册,挨个发短信)
var salesOffices = {
    clientList: [],
    listen(fn) {
        this.clientList.push(fn)
    },
    trigger() {
        for (let i = 0, fn; fn = this.clientList[i++];) {
            fn.apply(this, arguments);
        }
    }
};
salesOffices.listen(function (price, squareMeter) {
    console.log("价格" + price + '平方' + squareMeter);
});
salesOffices.listen(function (price, squareMeter) {
    console.log("价格" + price + '平方' + squareMeter);
})
salesOffices.trigger('3w', 88);
salesOffices.trigger('2w', 120);
复制代码

这里存在一个问题,如果小明只想订阅小户型却将所有的房价信息发送给了他,所以这里需要加一个标识key,让订阅者只订阅自己感兴趣的户型房价。

订阅指定key的发布订阅模式

var salesOffices = {
    clientList: [],
    listen(key, fn) {
        if (!this.clientList[key]) {
            this.clientList[key] = [];
        }
        this.clientList[key].push(fn)
    },
    trigger(key, ...args) {
        var fns = this.clientList[key]; // 当前的事件队列为
        // 如果没有订阅过该消息
        if (!fns && fns.length === 0) {
            return false;
        }
        // 调用当前fn
        for (let i = 0, fn; fn = fns[i++];) {
            fn.call(this, ...args);
        }
    }
}
salesOffices.listen('squareMeter88', (price) => {
    console.log("价格" + price);
});
salesOffices.listen('squareMeter120', (price) => {
    console.log("价格" + price);
});
salesOffices.trigger('squareMeter88', '2w');
salesOffices.trigger('squareMeter120', '3w');
复制代码

此时就实现了订阅者只发送自己感兴趣的事件了

通用的发布订阅的实现

var event = {
    clientList: [],
    listen: function (key, fn) {
        // 如果用户没有订阅该事件,则为该事件分配一个事件队列用于存放消息列表
        if (!this.clientList[key]) {
            this.clientList[key] = [];
        }
        this.clientList[key].push(fn); // 订阅的消息添加进缓存列表
    },
    trigger: function (key,...args) {
        var fns = this.clientList[key]; // 当前订阅事件的所有消息列表
        if (!fns || fns.length === 0) { // 如果没有绑定对应的消息
            return false;
        }
        for (var i = 0, fn; fn = fns[i++];) {
            fn.call(this, ...args); // (2) 
        }
    }
};
// 定义一个installEvent函数,这个函数可以给所有的对象都动态的安装发布-订阅模式
var installEvent = function(obj) {
    for(let i in obj) {
        event[i] = obj[i];
    }
}
// 这样不仅售楼部可以使用该工具,技术部如果想使用也可以直接安装installEvent方法

var salesOffices = {};
installEvent(salesOffices);

salesOffices.listen('squareMeter88', function (price) {
    console.log("价格" + price);
})

salesOffices.listen('squareMeter120', function (price) {
    console.log("价格" + price);
})

salesOffices.trigger('squareMeter88', '3w');
salesOffices.trigger('squareMeter120', '2w');

复制代码

移除订阅的事件

var event = {
    clientList: [],
    listen: function (key, fn) {
        // 如果用户没有订阅该事件,则为该事件分配一个事件队列用于存放消息列表
        if (!this.clientList[key]) {
            this.clientList[key] = [];
        }
        this.clientList[key].push(fn); // 订阅的消息添加进缓存列表
    },
    trigger: function (key,...args) {
        var fns = this.clientList[key]; // 当前订阅事件的所有消息列表
        if (!fns || fns.length === 0) { // 如果没有绑定对应的消息
            return false;
        }
        for (var i = 0, fn; fn = fns[i++];) {
            fn.call(this, ...args); // (2) 
        }
    },
    remove(key,fn) {
        var fns = this.clientList[key];
        // 如果key对应的事件没有被人订阅,则直接返回
        if(!fns) {
            return false;
        },
        // 如果没有回调函数则将该事件队列清空
        if(!fn) {
            fns && (fns.length == 0)
        } else {
            // 删除数组里的元素需要使用倒叙删除,不然会出现数组的飘移问题
            for(let k = fns.length - 1; k >= 0; k--) {
                var _fn = fns[k];
                // 如果当前的回调函数与事件队列的相同的则移除该项
                if(_fn = fn) {
                    fns.splice(k,1)
                }
            }
        }
    }
};
复制代码

Events模块简单实现

根据上面的发布订阅模式的基础简单的事件node的EventEmitter

function EventEmitter() {
    this._event = {};
}

EventEmitter.prototype.on = function (eventNames, cb) {
    if (!this._event) this._event = {};
    if (this._event[eventNames] || (this._event[eventNames] = [])) {
        this._event[eventNames].push(cb);
    }
}

EventEmitter.prototype.emit = function (eventNames, ...args) {
    if (this._event && this._event[eventNames]) {
        this._event[eventNames].forEach(events => {
            events.call(this,...args)
        });
    }
}

// off的本质是将this._events[eventName]重新赋值,赋值为filter过后的值
// 直接过滤,找到索引采用splice删除 .filter(cb => cb !== callback);
// 最后一定要记得es6模块的导入导出

EventEmitter.prototype.off = function (eventNames, callback) {
    if (this._event && this._event[eventNames]) {
        this._event[eventNames] = this._event[eventNames].filter((event) => {
            return (event !== callback && event.my !== callback))
        }
    }
}

// once的本质是先执行on让后触发off
EventEmitter.prototype.once = function (eventNames, callback) {
    // 需要等on触发完毕以后再触发off事件
    // this.on(eventNames, callback);
    // this.off(eventNames, callback)
    let once = (...args) => {
        callback(...args);
        // 此处调用的是off函数,传入的实际上是eat,需要删除的是once
        this.off(eventNames, once)
    }
    once.my = callback;
    this.on(eventNames, once)//先绑定一个一次性事件,稍后触发时,再将事件清空
}
module.exports = EventEmitter;

复制代码
文章分类
前端
文章标签