zoukankan      html  css  js  c++  java
  • 你必须了解的设计模式,发布订阅模式

    发布订阅模式,它其实是一种对象间一对多的依赖关系(不是综艺节目以一敌百那种),当一个对象的状态发送改变时,所有依赖于它的对象都将得到状态改变的通知。

    作用

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

    先从最简单的说起

    let corp = {};   // 自定义一个公司对象

    // 这里放一个列表用来缓存回调函数 corp.list = []; // 去订阅事件 corp.on = function (fn) { // 先把fn先存到上面那个列表中 this.list.push(fn); }; // 发布事件 corp.emit = function () { // 当发布的时候再把列表里存的函数依次执行 this.list.forEach(cb => { cb.apply(this, arguments); }); }; // 测试用例 corp.on(function (position, salary) { console.log('你的职位是:' + position); console.log('期望薪水:' + salary); }); corp.on(function(skill, hobby) { console.log('你的技能有: ' + skill); console.log('爱好: ' + hobby); }); corp.emit('前端', 10000); corp.emit('端茶和倒水', '足球'); /* 你的职位是:前端 期望薪水:10000 你的技能有: 前端 爱好: 10000 你的职位是:端茶和倒水 期望薪水:足球 你的技能有: 端茶和倒水 爱好: 足球 */

    上面通过自定义事件实现了一个简单的发布订阅模式,不过从打印出来的结果来看,不能尽如人意。

    因为我们希望打印是这样子的:

    /*
        你的职位是:前端
        期望薪水:10000
        
        你的技能有: 端茶和倒水
        爱好: 足球
    */

    原因,在on方法的时候,将fn函数全部放到了列表中。

    不怕,只要一个简单的key值,这个问题就可以解决。让我们一起改写一下上面的代码吧!

    let corp = {};
    // 这次换成一个对象类型的缓存列表
    corp.list = {};
    corp.on = function(key, fn) {
        // 如果对象中没有对应的key值
        // 也就是说明没有订阅过
        // 那就给key创建个缓存列表
        if (!this.list[key]) {
            this.list[key] = [];
        }
        // 把函数添加到对应key的缓存列表里
        this.list[key].push(fn);
    };
    corp.emit = function() {
        // 第一个参数是对应的key值
        // 直接用数组的shift方法取出
        let key = [].shift.call(arguments),
            fns = this.list[key];
        // 如果缓存列表里没有函数就返回false
        if (!fns || fns.length === 0) {
            return false;
        }
        // 遍历key值对应的缓存列表
        // 依次执行函数的方法
        fns.forEach(fn => {
            fn.apply(this, arguments);
        });
    };
    
    // 测试用例
    corp.on('join', (position, salary) => {
        console.log('你的职位是:' + position);
        console.log('期望薪水:' + salary);
    });
    corp.on('other', (skill, hobby) => {
        console.log('你的技能有: ' + skill);
        console.log('爱好: ' + hobby);
    });
    
    corp.emit('join', '前端', 10000);
    corp.emit('join', '后端', 10000);
    corp.emit('other', '端茶和倒水', '足球');
    /*
        你的职位是:前端
        期望薪水:10000
        你的职位是:后端
        期望薪水:10000
        你的技能有: 端茶和倒水
        爱好: 足球
    */

    good~!

    现在来搞个通用的发布订阅模式实现,和刚才的差不多,不过这次起要隆重些,叫event,请看代码~

    let event = {
        list: {},
        on(key, fn) {
            if (!this.list[key]) {
                this.list[key] = [];
            }
            this.list[key].push(fn);
        },
        emit() {
            let key = [].shift.call(arguments),
                fns = this.list[key];
    
            if (!fns || fns.length === 0) {
                return false;
            }
            fns.forEach(fn => {
                fn.apply(this, arguments);
            });
        },
        remove(key, fn) {
            // 这回我们加入了取消订阅的方法
            let fns = this.list[key];
            // 如果缓存列表中没有函数,返回false
            if (!fns) return false;
            // 如果没有传对应函数的话
            // 就会将key值对应缓存列表中的函数都清空掉
            if (!fn) {
                fns && (fns.length = 0);
            } else {
                // 遍历缓存列表,看看传入的fn与哪个函数相同
                // 如果相同就直接从缓存列表中删掉即可
                fns.forEach((cb, i) => {
                    if (cb === fn) {
                        fns.splice(i, 1);
                    }
                });
            }
        }
    }
    
    function cat() {
        console.log('一起喵喵喵');
    }
    function dog() {
        console.log('一起旺旺旺');
    }
    
    event.on('pet', data => {
        console.log('接收数据');
        console.log(data);
    });
    event.on('pet', cat);
    event.on('pet', dog);
    // 取消dog方法的订阅
    event.remove('pet', dog);
    // 发布
    event.emit('pet', ['二哈', '波斯猫']);
    /*
        接收数据
        [ '二哈', '波斯猫' ]
        一起喵喵喵
    */

    这样已经实现了一个可以使用的发布订阅模式了,来一起屡屡思路吧!

    思路:

    • 创建一个对象(缓存列表)
    • on方法用来把回调函数fn都加到缓存列表中
    • emit方法取到arguments里第一个当做key,根据key值去执行对应缓存列表中的函数
    • remove方法可以根据key值取消订阅

    接下来我们来看看node中的events模块,这个在node中真的是很重要的模块!在使用后发现,这完全是个大写的发布订阅模式啊。

    先来一个测试用例~

    // {'失恋',  [findboy, drink]}
    // 监听的目的 就是为了构造这样一个对象 一对多的关系    on
    // 发布的时候 会让数组的函数依次执行    emit
    // [findboy, drink]
    // 这里用接下来我们写的
    // let EventEmitter = require('./events');
    let EventEmitter = require('events');
    let util = require('util');
    
    function Girl() {
    }
    // Girl继承EventEmitter上的方法
    util.inherits(Girl, EventEmitter);  // 相当于Girl.prototype.__proto__ = EventEmitter.prototype
    let girl = new Girl();
    let drink = function (data) {
        console.log(data);
        console.log('喝酒');
    };
    let findboy = function () {
        console.log('交友');
    };
    
    girl.on('newListener', function (eventName) {
        // console.log('名称: ' + eventName);
    });
    girl.on('结婚', function() {});
    girl.setMaxListeners(3); //可以为指定的 EventEmitter 实例修改限制。 值设为 Infinity(或 0)表示不限制监听器的数量。
    console.log(girl.getMaxListeners());
    girl.once('失恋', drink);       // {'失恋': [drink]}
    girl.once('失恋', drink);       // {'失恋': [drink]}
    girl.prependListener('失恋', function () {
        console.log('before');
    });
    girl.once('失恋', drink);       // {'失恋': [drink]}
    girl.emit('失恋', '1');

    以上代码是events核心模块的使用方法,快快动手敲起来哈!

    接下来我们来手写一个node的EventEmitter吧

    function EventEmitter() {
        // 用Object.create(null)代替空对象{}
        // 好处是无杂质,不继承原型链的东东
        this._events = Object.create(null);
    }
    // 默认最多的绑定次数
    EventEmitter.defaultMaxListeners = 10;
    // 同on方法
    EventEmitter.prototype.addListener = EventEmitter.prototype.on;
    // 返回监听的事件名
    EventEmitter.prototype.eventNames = function () {
        return Object.keys(this._events);
    };
    // 设置最大监听数
    EventEmitter.prototype.setMaxListeners = function (n) {
        this._count = n;
    };
    // 返回监听数
    EventEmitter.prototype.getMaxListeners = function () {
        return this._count ? this._count : this.defaultMaxListeners;
    };
    // 监听
    EventEmitter.prototype.on = function (type, cb, flag) {
        // 默认值,如果没有_events的话,就给它创建一个
        if (!this._events) {
            this._events = Object.create(null);
        }
        // 不是newListener 就应该让newListener执行以下
        if (type !== 'newListener') {
            this._events['newListener'] && this._events['newListener'].forEach(listener => {
                listener(type);
            });
        }
        if (this._events[type]) {
            // 根据传入的flag来决定是向前还是向后添加
            if (flag) {
                this._events[type].unshift(cb);
            } else {
                this._events[type].push(cb);
            }
        } else {
            this._events[type] = [cb];
        }
        // 监听的事件不能超过了设置的最大监听数
        if (this._events[type].length === this.getMaxListeners()) {
            console.warn('警告-警告-警告');
        }
    };
    // 向前添加
    EventEmitter.prototype.prependListener = function (type, cb) {
        this.on(type, cb, true);
    };
    EventEmitter.prototype.prependOnceListener = function (type, cb) {
        this.once(type, cb, true);
    };
    // 监听一次
    EventEmitter.prototype.once = function (type, cb, flag) {
        // 先绑定,调用后删除
        function wrap() {
            cb(...arguments);
            this.removeListener(type, wrap);
        }
        // 自定义属性
        wrap.listen = cb;
        this.on(type, wrap, flag);
    };
    // 删除监听类型
    EventEmitter.prototype.removeListener = function (type, cb) {
        if (this._events[type]) {
            this._events[type] = this._events[type].filter(listener => {
                return cb !== listener && cb !== listener.listen;
            });
        }
    };
    EventEmitter.prototype.removeAllListener = function () {
        this._events = Object.create(null);
    };
    // 返回所有的监听类型
    EventEmitter.prototype.listeners = function (type) {
        return this._events[type];
    };
    // 发布
    EventEmitter.prototype.emit = function (type, ...args) {
        if (this._events[type]) {
            this._events[type].forEach(listener => {
                listener.call(this, ...args);
            });
        }
    };
    
    module.exports = EventEmitter;

    上面我们实现了node的核心模块events,完成了EventEmitter的功能!

    总结

    优点

    • 对象之间的解耦
    • 异步编程中,可以更松耦合的代码编写

    缺点

    • 创建订阅者本身要消耗一定的时间和内存
    • 多个发布者和订阅者嵌套一起的时候,程序难以跟踪维护

    过度使用的话,会出现上述缺点的问题。不过合理开发合理利用,这不是什么大问题的。

    好好利用这个最常见的模式吧,升华你的编程!

  • 相关阅读:
    Splay 详解
    莫队套值域分块
    浅谈区间众数
    回滚莫队分块
    带修莫队分块
    微服务规划准则
    mysql查询包含逗号的数据,并逗号拆分为多行展现
    python mysql 单连接和连接池简单示例
    代理模式八:装饰者模式
    代理模式七:迭代器模式
  • 原文地址:https://www.cnblogs.com/magicg/p/12612338.html
Copyright © 2011-2022 走看看