定义
又称观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时候,其他依赖于它的对象都将得到通知。在JS开发中,一般用事件模型来代替传统的发布-订阅模式。
场景
DOM事件
document.body.addEventListener('click',function(){
console.log("点击了body");
},false);
document.body.click(); // 模拟用户点击
这其实就是一种简单的发布-订阅模式
自定义实现一个发布-订阅模式
var saleOffices = {}; // 定义售楼部
saleOffices.clientList = []; // 缓存列表,存放订阅者的回调函数
saleOffices.listen = function(fn){ // 增加订阅者
this.clientList.push(fn); // 订阅的消息添加进缓存列表
}
saleOffices.trrigger = function(){ // 发布消息
for(var i=0,fn;fn=this.clientList[i++];){
fn.apply(this,arguments); // arguments是发布消息时带上的参数
}
}
// 开始简单测试
saleOffices.listen(function(price,squareMeter){ // 小明订阅消息
console.log('价格='+price);
console.log('squareMeter='+squareMeter);
})
saleOffices.listen(function(price,squareMeter){ // 小红订阅消息
console.log('价格='+price);
console.log('squareMeter='+squareMeter);
})
// 发布消息
saleOffices.trrigger(20000,'小明');
saleOffices.trrigger(8000,'小红');
发布-订阅模式的通用实现
发布只有一个,订阅者有多个,使用数组存储多个订阅者
var event = {
clientList:[],
// 订阅
listen(key,fn){
if(!this.clientList[key]){
this.clientList[key] = [];
};
this.clientList[key].push(fn);
},
// 发布消息
trigger(){
var key = Array.prototype.shift.call(arguments); // 获取arguments的第一个值
var fns = this.clientList[key];
if(!fns || fns.length === 0){ // 没有绑定消息,return false
return false;
}
for(var i=0,fn;fn=fns[i++];){
fn.apply(this,arguments);
}
},
// 取消订阅事件,就是移除当前在数组中的订阅事件
remove(key,fn){
var fns = this.clientList[key];
if(!fns) return false; // 没有该订阅事件也删除不了什么
if(!fn){ // 如果没有传入具体的回调函数,表示需要取消key对应消息的所有订阅
fns && (fns.length = 0); // 作者将数组清空直接定义数组的length属性为0
}else{
for(var l = fns.length-1;l>=0;l--){
var _fn = fns[l];
if(_fn === fn){
fns.splice(l,1); // 删除订阅者的回调函数
}
}
}
}
}
// 测试模式
var installEvent = function(obj){
for(var i in event){
obj[i] = event[i];
}
}
var salesOffices = {};
installEvent(salesOffices);
// 订阅
salesOffices.listen('squareMeter88',fn1 = function(price){
console.log('价格=',price);
})
salesOffices.listen('squareMeter100',function(price){
console.log('价格=',price);
});
// 发布消息
salesOffices.remove('squareMeter88',fn1);
salesOffices.trigger('squareMeter88',20000);
salesOffices.trigger('squareMeter100',30000);
再看一个例子
一个典型的观察者模式引用场景是用户在一个网站订阅主题。
- 多个用户(观察者,Observer)都可以订阅某个主题(Subject)
- 当主题内容更新时订阅该主题的用户都能收到通知。
代码设计:
ES5版本
function Subject(){
// 存放众多观察者的数组
this.observers = [];
}
// 添加一个观察者
Subject.prototype.addObserver = function(observer){
this.observers.push(observer);
}
// 删除当前观察者
Subject.prototype.removeObserver = function(observer){
var index = this.observers.indexOf(observer);
if(index>-1){
this.observers.splice(index,1);
}
}
// 发布消息给众多的订阅者
Subject.prototype.notify = function(){
this.observers.forEach(function(observer){
observer.update();
})
}
// 创建订阅者函数
function Observer(name){
this.name = name;
this.update = function(){
console.log(name+'update...');
}
}
// 创建主题
var subject = new Subject();
// 创建观察者1
subject.addObserver(new Observer('主题一'));
// 创建观察者2
subject.addObserver(new Observer('主题二'));
// 订阅完毕之后发布主题变动消息
subject.notify();
看来很多的设计模式都是函数式编程的写法。如果是曾探的话会引用很多对象函数式调用写法。
换成ES6的写法
class Subject{
constructor(){
this.observers = [];
}
addObserver(observer){
this.observers.push(observer);
}
removeObserver(observer){
var index = this.observers.indexOf(observer);
if(index>-1){
this.observers.splice(index,1);
}
}
notify(){
this.observers.forEach(function(observer){
observer.update();
})
}
}
class Observer{
constructor(name){
this.name = name;
}
update(){
console.log(this.name+'update...');
}
}
var subject = new Subject();
subject.addObserver(new Observer('主题一'));
subject.addObserver(new Observer('主题二'));
subject.notify();
这是一个简易的Vue如何使用数据劫持和发布订阅者模式实现mvvm的
Vue会遍历实例中的data,把每一个data都设置为访问器,然后在该属性的getter函数中将其设为wather,在setter中向其他wether发布改变的消息。
再配合订阅/发布模式,改变其中一个值,会发布消息,所有的watcher会更新自己。这些watcher也就是绑定在dom中的显示信息。
// 遍历传入实例的data属性,将其设置为vue对象的访问器属性
function observer(obj,vm){
Object.keys(obj).forEach(key=>{
defineReactive(vm,key,obj[key]);
})
}
// 设置为访问器属性,并在其getter和setter函数中,使用订阅发布模式。相互监听
function defineReactive(obj,key,val){
// 开始使用订阅/发布模式
// 实例化一个主题对象,对象中有空的观察者列表
var dep = new Dep();
// 将data中的每一个属性都设置为vue对象的访问器属性,属性名和data中相同。
Object.defineProerty(obj,key,{
get(){
// Dep.target指针指向watcher,增加订阅者watcher到主体对象Dep
if(Dep.target) dep.addSub(Dep.target);
return val;
},
set(newVal){
// 没有变化就return,不执行下面
if(newVal === val) return;
val = newVal;
// set触发之后给订阅者列表中的watchers发出通知
dep.notify();
}
})
}
// 主题对象Dep构造函数
function Dep(){
// 存放所有的订阅者
this.subs = [];
}
// 添加订阅者
Dep.prototype.addSub = function(sub){
this.subs.push(sub);
}
// 通知所有的订阅者
Dep.prototype.notify = function(sub){
this.subs.forEach(sub=>{
sub.update();
})
}
// 应该还缺少删除订阅者的操作
总结一下:
Vue将实例化中的data每一个都去做数据接触,用defineProperty
去数据接触。在每一项的getter
中去添加订阅者,在每一项的setter
方法中做判断,如果数据被更改,就去触发所有的发布。
发布/订阅模式的有点非常明显:
- 时间上的解耦
- 对象之间的解耦
应用非常广泛,因为JS本身就是一门基于事件驱动的语言。不管是MVC还是MVVM都少不了这已模式的参与。
当然发布/订阅模式也有确定。创建订阅者本身要消耗一定的时间和内存,而当你订阅了一个消息后,也许此消息最后都没有发生过。但是这个订阅会始终存在内存中。
另外订阅/发布会弱化对象之间的联系,如果过度使用的话,对象和对象之间的必要联系也将被深埋在背后,最终导致程序难以跟踪和维护。