观察者模式
观察者模式又叫发布订阅模式(Publish/Subscribe),它定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使得它们能够自动更新自己。
使用观察者模式的好处:
- 支持简单的广播通信,自动通知所有已经订阅过的对象。
- 页面载入后目标对象很容易与观察者存在一种动态关联,增加了灵活性。
- 目标对象与观察者之间的抽象耦合关系能够单独扩展以及重用。
理解的第一阶段
我们先用一个通俗的情景来比喻一下,就拿 航班 与 航站楼 之间的关系:
图上显示的是 飞机1在从 北京 飞往 南京, 北京航站楼A 和 南京航站楼B 都需要关注 飞机1 的信息,
而 飞机1 在位置变化 之后也要及时的 通知 北京航站楼A 和 南京航站楼。
(其实可以看出来,这里 航站楼是被动的接受 飞机是主动的发出变化)
所以,我们可以写一个简单的 观察/订阅模式
function Observer () {
// 数组存放多个观察者
this.fns = [];
}
Observer.prototype = {
// 订阅 (飞机信息)
sub: function (fn) {
this.fns.push(fn);
},
// 退订 (飞机信息)
unsub: function (fn) {
this.fns = this.fns.filter(function(el) {
if (el !== fn) {
return el;
}
})
},
// 发布 (飞机信息变化时)
publish: function (o, thisObj) {
var scope = thisObj || window;
this.fns.forEach(function(el) {
el.call(scope, o);
});
}
}
var ob = new Observer;
初始化 飞机1 的地理位置
var location = '北京';
定义 2 个航站楼的行为
// 北京 航站楼A - 观察者
var hangzhanlouBeijing = function (data) {
console.log('北京航站楼 收到了数据变化的通知,我知道现在 飞机1 的位置了,是在:' +data);
// 做一些操作
};
// 南京 航站楼B - 观察者
var hangzhanlouNanjing= function (data) {
console.log('南京航站楼 收到了数据变化的通知,我知道现在 飞机1 的位置了,是在:' +data + ',我这就准备接机');
// 做一些操作
};
南京和北京航站楼开始订阅(注册),将要关注 飞机1 的地址位置
// 订阅 (飞机地理位置的变化)
ob.sub(hangzhanlouBeijing);
ob.sub(hangzhanlouNanjing);
飞机1 经过一段时间的飞行 从 北京 到了 南京 ,这时候地理位置发生了变化
// 经过一些 操作 导致 location 发生了变化
location = '南京';
重要 然后 检查(校验) location(地址位置) 是否发生变化,如果发生变化,就进行通知
// 数据 location 发生变化(现在的值 不等于 原来的值)
if (location!== '北京') {
// 发布 数据变化了(确认地址位置变了 ),广播 所有订阅(关注) 飞机1 的观察者(航站楼)
ob.update(location);
}
输出
北京航站楼 收到了数据变化的通知,我知道现在 飞机1 的位置了,是在: 南京
南京航站楼 收到了数据变化的通知,我知道现在 飞机1 的位置了,是在: 南京,我这就准备接机
理解的第二阶段
情况一:
因为这里我们只有一架 飞机1 具有了 观察者模式,如果再来一架 飞机2 也是从 北京 飞往 南京 的呢?很明显,我们也需要知道 飞机2 的地理位置信息
情况二:
还有假如 北京航站楼 只关注 飞机1 的变化,而不关注 飞机2 的变化,同理, 南京航站楼 只关注 _飞机2_的变化, 而不关注 飞机1 的变化
那这样情况就比较复杂了
学术一点的意思就是:要让多个对象都具有观察者模式(让 多个 观察者
可以关注 多个 自己想关注的 观察对象
)。
这时候分析下,相比较之前有什么变化
- 首先,存放 观察者(航站楼) 的数组 的个数不能固定,且类别/附属 应该 与 观察对象 关联挂钩
- 再者,观察对象 的 被观察属性 也不应该固定(之前只关注了 飞机1 的 location ,比如,我们可以观察 飞机 的 地理位置,同样也可以关注 飞机 的 海拔高度 等信息)
用图表示 应该就是下面这种 数据情景
所以这里 我们定义的一个通用函数 ,它应该具备 上述的 一些特性
- 多个观察者
- 多个观察者对象的多个属性
- 给 他们添加 订阅,取消订阅,发布的方法
也就是说:每个观察者对象,都独立维护一套独立的观察者模式。
根据需要,我们抽象出下面 这样一个通用的函数:
//通用代码
var observer = {
//订阅
addSubscriber: function (callback) {
this.subscribers[this.subscribers.length] = callback;
},
//退订
removeSubscriber: function (callback) {
for (var i = 0; i < this.subscribers.length; i++) {
if (this.subscribers[i] === callback) {
delete (this.subscribers[i]);
}
}
},
//发布
publish: function (what) {
for (var i = 0; i < this.subscribers.length; i++) {
if (typeof this.subscribers[i] === 'function') {
this.subscribers[i](what);
}
}
},
// 通过遍历,给 观察者对象 o 添加 观察者容器,订阅,退订,发布
make: function (o) {
for (var i in this) {
o[i] = this[i];
// 每个 观察者对象 都维护自身的一个 观察者 数组
o.subscribers = [];
}
}
};
OK,已经抽象出了 具体的 范式,下面就结合具体情景来看下看是否符合
下面 先分别定义 飞机1,飞机2 这两个观察者对象的 多个属性(地理位置 和 海拔)
var plan1 = {
location: '北京',
height: '200km'
}
var plan2 = {
location: '云南',
height: '100km'
}
再,分别定义 观察者 的行为(航站楼1,航站楼2 分别关注 飞机的运行状态)
var hangzhanlou1 = function hangzhanlou1(cb) {
console.log('航站楼1 收到了 我关注的 飞机 的运行状态发生了改变...')
// 订阅的对象发生了变化 , 观察者 做一些自己想做的事...
cb();
}
var hangzhanlou2 = function hangzhanlou2 (cb) {
console.log('航站楼2 收到了 我关注的 飞机 的运行状态发生了改变...')
// 订阅的对象发生了变化 , 观察者 做一些自己想做的事...
cb();
}
这里,航站楼1 可以 订阅 飞机1,也可以订阅 飞机2 ,也可以都订阅
同样,航站楼1 可以订阅 飞机的 位置,也可以订阅飞机的 海拔, 也可以都订阅
- 现实层面上:飞机运行,发生状态的变化
- Web层面上:一系列事件操作导致的状态变化
给 飞机1,飞机2的 地理位置,海拔高度 都 添加 观察者模式
observer.make(plan1);
observer.make(plan2);
添加 观察者 hangzhanlou1
plan1.addSubscriber(hangzhanlou1)
飞机1 经过飞行,判断 关注的信息是否发生了变化
这里判断是伪代码,但是这个判断的逻辑非常!非常!非常!的重要!如果有时间,这个后续会展开讲解,因为这里是另外一个核心内容,diff变化,从而控制View层的渲染!
// 这里为了演示方便,默认判断 地理,海拔都变化了(其实也可以判断 观察者某一个 属性)
function checkChange(val, oldVal) {
if (val !== oldVal) {
plan1.publish(function(){
console.log('飞机1 现在位置是上海,海拔是100km');
})
}
}
// 航站楼1 收到了 我关注的 飞机 的运行状态发生了改变...
// 飞机1 现在位置是上海,海拔是100km
plan1.addSubscriber(hangzhanlou2)
同样判断状态的改变,进行发布
function checkChange(val, oldVal) {
if (val !== oldVal) {
plan1.publish(function(){
console.log('飞机1 现在位置是太原,海拔是120km');
})
}
}
// 航站楼1 收到了 我关注的 飞机 的运行状态发生了改变...
// 飞机1 现在位置是太原,海拔是120km
// 航站楼2 收到了 我关注的 飞机 的运行状态发生了改变...
// 飞机1 现在位置是太原,海拔是120km
添加 观察者 hangzhanlou1
plan2.addSubscriber(hangzhanlou1)
同样判断状态的改变,进行发布
function checkChange1(val, oldVal) {
if (val !== oldVal) {
plan1.publish(function(){
console.log('飞机1 现在位置是青海,海拔是300km');
})
}
}
// 航站楼1 收到了 我关注的 飞机 的运行状态发生了改变...
// 飞机1 现在位置是青海,海拔是300km
// 航站楼2 收到了 我关注的 飞机 的运行状态发生了改变...
// 飞机1 现在位置是青海,海拔是300km
function checkChange2(val, oldVal) {
if (val !== oldVal) {
plan2.publish(function(){
console.log('飞机2 现在位置是西藏,海拔是500km');
})
}
}
// 航站楼2 收到了 我关注的 飞机 的运行状态发生了改变...
// 飞机2 现在位置是西藏,海拔是500km
添加 观察者 hangzhanlou2
plan2.addSubscriber(hangzhanlou2)
同样判断状态的改变,进行发布
function checkChange1(val, oldVal) {
if (val !== oldVal) {
plan1.publish(function(){
console.log('飞机1 现在位置是青海,海拔是300km');
})
}
}
// 航站楼1 收到了 我关注的 飞机 的运行状态发生了改变...
// 飞机1 现在位置是青海,海拔是300km
// 航站楼2 收到了 我关注的 飞机 的运行状态发生了改变...
// 飞机1 现在位置是青海,海拔是300km
function checkChange2(val, oldVal) {
if (val !== oldVal) {
plan2.publish(function(){
console.log('飞机2 现在位置是西藏,海拔是500km');
})
}
}
// 航站楼1 收到了 我关注的 飞机 的运行状态发生了改变...
// 飞机2 现在位置是西藏,海拔是500km
// 航站楼2 收到了 我关注的 飞机 的运行状态发生了改变...
// 飞机2 现在位置是西藏,海拔是500km
- 情景5:航站楼2 取消订阅 飞机1(因为飞机1已经 安全降落了)
在 飞机1 上移除 观察者 hangzhanlou2
plan1.removeSubscriber(hangzhanlou2)
最后
观察者模式大体就是如此,但是在开发中为了适应各种场景可能会有很多变种,但是万变不离其中。
上面的代码只是用来帮助理解的,针对 航班 与 航站楼 这种情景 还有很多改进的地方。
以上只是个人理解的一些拙见,如有不对之处还请海涵,并希望大家帮忙纠正!