JavaScript设计模式

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

发布订阅模式也叫观察者模式,但是两者之间有些区别。
观察者模式和发布订阅模式最大的区别就是发布订阅模式有个事件调度中心。

file

从图中可以看出,观察者模式中观察者和目标直接进行交互,而发布者模式中统一由调度中心进行处理,订阅者和发布者互不干扰。

这样一方面实现了解耦,还有就是可以实现更细粒度的一些控制。比如发布者发布了很多消息,但是不想所有的订阅都接收到,就可以在调度中心做一些处理,类似于权限控制之类的。还可以做一些节流操作。

观察者模式

它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在JavaScript开发中,我们一般用事件模型来替代传统的发布-订阅模式。

// 观察者
class Observer {
    constructor() {}
    update(val) {

    }
}
// 观察者列表
class ObserverList {
    constructor() {
        this.observerList = []
    }
    add(observer) {
        return this.observerList.push(observer);
    }
    remove(observer) {
        this.observerList = this.observerList.filter(ob => ob !== observer);
    }
    count() {
        return this.observerList.length;
    }
    get(index) {
        return this.observerList[index];
    }
}
// 目标
class Subject {
    constructor() {
        this.observers = new ObserverList();
    }
    addObserver(observer) {
        this.observers.add(observer);
    }
    removeObserver(observer) {
        this.observers.remove(observer);
    }
    notify(...args) {
        let obCount = this.observers.count();
        for (let index = 0; index < obCount; index++) {
            this.observers.get(i).update(...args);
        }
    }
}

小结:

1.观察者模式可以使代码解耦,满足开放封闭原则
2.当过多的使用观察者模式,如果订阅消息始终都没有触发,则订阅者一直保存在内存中。

发布订阅模式

也称作观察者模式,定义了对象间的一种一对多的依赖关系,当一个对象的状态发 生改变时,所有依赖于它的对象都将得到通知

取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口。

与传统的发布-订阅模式实现方式(将订阅者自身当成引用传入发布者)不同,在JS中通常使用注册回调函数的形式来订阅

class PubSub {
    constructor() {
        this.subscribers = {}
    }
    subscribe(type, fn) {
        if (!Object.prototype.hasOwnProperty.call(this.subscribers, type)) {
          this.subscribers[type] = [];
        }
        
        this.subscribers[type].push(fn);
    }
    unsubscribe(type, fn) {
        let listeners = this.subscribers[type];
        if (!listeners || !listeners.length) return;
        this.subscribers[type] = listeners.filter(v => v !== fn);
    }
    publish(type, ...args) {
        let listeners = this.subscribers[type];
        if (!listeners || !listeners.length) return;
        listeners.forEach(fn => fn(...args));        
    }
}

let ob = new PubSub();
ob.subscribe('add', (val) => console.log(val));
ob.publish('add', 1);

JS中的事件就是经典的发布-订阅模式的实现

// 订阅
document.body.addEventListener('click', function() {
    console.log('click1');
}, false);

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

// 发布
document.body.click(); // click1  click2

现实类比:

小A在公司C完成了笔试及面试,小B也在公司C完成了笔试。他们焦急地等待结果,每隔半天就电话询问公司C,导致公司C很不耐烦。一种解决办法是 AB直接把联系方式留给C,有结果的话C自然会通知AB

这里的“询问”属于显示调用,“留给”属于订阅,“通知”属于发布

// 观察者
var observer = {
    // 订阅集合
    subscribes: [],

    // 订阅
    subscribe: function(type, fn) {
        if (!this.subscribes[type]) {
            this.subscribes[type] = [];
        }
        
        // 收集订阅者的处理
        typeof fn === 'function' && this.subscribes[type].push(fn);
    },

    // 发布  可能会携带一些信息发布出去
    publish: function() {
        var type = [].shift.call(arguments),
            fns = this.subscribes[type];
        
        // 不存在的订阅类型,以及订阅时未传入处理回调的
        if (!fns || !fns.length) {
            return;
        }
        
        // 挨个处理调用
        for (var i = 0; i < fns.length; ++i) {
            fns[i].apply(this, arguments);
        }
    },
    
    // 删除订阅
    remove: function(type, fn) {
        // 删除全部
        if (typeof type === 'undefined') {
            this.subscribes = [];
            return;
        }

        var fns = this.subscribes[type];

        // 不存在的订阅类型,以及订阅时未传入处理回调的
        if (!fns || !fns.length) {
            return;
        }

        if (typeof fn === 'undefined') {
            fns.length = 0;
            return;
        }

        // 挨个处理删除
        for (var i = 0; i < fns.length; ++i) {
            if (fns[i] === fn) {
                fns.splice(i, 1);
            }
        }
    }
};

// 订阅岗位列表
function jobListForA(jobs) {
    console.log('A', jobs);
}

function jobListForB(jobs) {
    console.log('B', jobs);
}

// A订阅了笔试成绩
observer.subscribe('job', jobListForA);
// B订阅了笔试成绩
observer.subscribe('job', jobListForB);


// A订阅了笔试成绩
observer.subscribe('examinationA', function(score) {
    console.log(score);
});

// B订阅了笔试成绩
observer.subscribe('examinationB', function(score) {
    console.log(score);
});

// A订阅了面试结果
observer.subscribe('interviewA', function(result) {
    console.log(result);
});

observer.publish('examinationA', 100); // 100
observer.publish('examinationB', 80); // 80
observer.publish('interviewA', '备用'); // 备用

observer.publish('job', ['前端', '后端', '测试']); // 输出A和B的岗位


// B取消订阅了笔试成绩
observer.remove('examinationB');
// A都取消订阅了岗位
observer.remove('job', jobListForA);

observer.publish('examinationB', 80); // 没有可匹配的订阅,无输出
observer.publish('job', ['前端', '后端', '测试']); // 输出B的岗位

优缺点

优点: 一为时间上的解耦,二为对象之间的解耦。可以用在异步编程中与MV*框架中

缺点: 创建订阅者本身要消耗一定的时间和内存,订阅的处理函数不一定会被执行,驻留内存有性能开销

弱化了对象之间的联系,复杂的情况下可能会导致程序难以跟踪维护和理解

JS观察者模式

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>观察者模式</title>
	<style>
    #cont,#ad,#ft {
        height: 100px;
        margin-top: 20px;
        border: 1px solid green;
    }
    </style>
</head>
<body>
	<select id="sel" name="observers">
        <option value="m">men</option>
        <option value="f">women</option>
    </select>
    <div id="cont">这是内容,随男女发生背景变化</div>
    <div id="ad">这是广告,根据男女不同推荐相应的产品</div>
    <div id="ft">我是底部区域</div>
	<script>
		function Observed(){
			this.observers = [];
		}

		Observed.prototype = {
			//给被观察者添加一个注册方法,
    		//把观察他的对象注册到他的observers属性上
			attach: function(key,observer) {
				this.observers[key] = observer;
			},
			//注销观察者的方法,与attach相反
			detach: function(key) {
				delete this.observers[key];
			},
			// 当被观察者发生变化时,通知所有观察者:自己发生了那些变化
			notify: function() {
				for (var key in this.observers) {
					this.observers[key].update(this);
				}
			},

			/**
		     * 疑问: 你怎么知道观察者都有update方法呢?
		     * 在js里没有接口这个概念,事实上,如果是java或PHP的话,要用观察者模式
		     * 要先实现观察者接口
		     * interface observers{attach(),detach(),notify()}   被观察者
		     * interface observer{update()}   观察者
		     */
		}
		// 修改constructor的指向
		Observed.prototype.constructor = Observed;

		var sel = document.getElementById('sel');
	    var cont = document.getElementById('cont');
	    var ad = document.getElementById('ad');
	    var ft = document.getElementById('ft');

	    // 创建被观察者对象
		var observed = new Observed();
		
		// 被观察者对象上注册观察者
		observed.attach('cont',cont);
		observed.attach('ad', ad);
		observed.attach('ft', ft);
		// 通知观察者变化
		sel.onchange = observed.notify.bind(observed);

		// 多个观察者更新
		cont.update = function() { //接收被观察者对象
	        if (sel.value === 'm') { //men
	            this.innerHTML = "男士风格";
	            this.style.backgroundColor = "gray";
	            this.style.color = "#fff";
	        } else if (sel.value === 'f') {
	            this.innerHTML = "女士风格";
	            this.style.backgroundColor = "pink";
	        }
	    }

	    ad.update = function() { //接收被观察者对象
	        if (sel.value === 'm') { //men
	            this.innerHTML = "岛国";
	        } else if (sel.value === 'f') {
	            this.innerHTML = "减肥";
	        }
	    }
	    
	    ft.update = function() { //接收被观察者对象
	        this.innerHTML = "我在不停的在发生变化" + Math.random() * 10;
	    }

	    /**
	     * 总结: 
	     * 1.被观察者: attach(),detach(),notify(),observes{}
	     * 2.观察者: update()
	     * 3. 把观察者注册到被观察者身上,当被观察者发生变化时通知所有的观察者
	     * 4. 对象与对象之间应该互相通信而不是控制。
	     */
	</script>
</body>
</html>