1、解决的问题
Javascript在进行应用开发时是基于事件驱动的,在处理稍微复杂一点的逻辑时,就有可能出现 在某个事件的完成阶段要处理若干个事件回调的情况。
可能会出现这样的代码:
btn.onclick = function() {
sideBar.style.backgroundColor = "red";
sideBar.name = "new_sideBar";
foot.style.backgroundColor = "red";
foot.name = "new_name";
// ....
}
或者这样
btn.onclick = function() {
dealSideBar();
dealFoot();
// ...
}
function dealSideBar() {};
function dealFoot() {};
// ...
以上两种写法相同,都是把要做的事情交托给事件侦听器来做,在处理复杂逻辑时,上述的dealXXX()可能就会达到7个8个,甚至更多,代码都写在一个javascript文件中,显得冗长,因此,上述写法适合相对简单的应用。
当应用变复杂,或者我们需要在事件到达某种状态时按消息类型通知不同的子组件,子系统时,就可以使用Observer模式,Observer模式就是各个子组件要做的事情交还给子组件本身,仅提供订阅消息(事件状态变化),与取消订阅的服务。
不太理解Observerable的这种命名方式,我用Publisher来代替把。
class Publisher {
constructor(obj) {
this.obj = obj;
this._listeners = new Map();
}
add(e, fn) {
if (!this._listeners.has(e)) this._listeners.set(e, [fn]);
else this._listeners.get(e).push(fn);
}
notify(e) {
const fns = this._listeners.get(e.type);
if (fns){
for (let fn of fns) {
fn();
}
}
}
}
// 子组件自由决定 是否订阅,以及消息通知到达后要做的动作
测试样例
const btn = document.getElementById("btn");
const pub = new Publisher(btn);
btn.onclick = e => pub.notify(e);
// 子组件sidebar
const sideBar = document.getElementById("sideBar");
pub.add("click", function(){
sideBar.style.background = "red";
});
// 子组件foot
const foot = document.getElementById("foot");
pub.add("click", function(){
foot.style.background = "orange";
})
Observer模式将操作的责任分配给了组件本身(订阅者),而发布者只负责管理订阅列表与通知订阅列表。
Observer有两种应用:基于事件模型、基于我们自己写的对象
以下分开讨论
2、基于事件模型应用
(1)Dom事件
// 事件监听器是发布者,body是订阅者
document.body.onclick = function() {...}
// 事件状态改变,命中订阅,执行回调
document.body.click();
(2)自定义事件
例子源自《JavaScript设计模式与开发实践》一书,大致场景为顾客要买房,但心怡面积和价格的房子目前售罄,需要等销售后续通知再进行购买。
以下为我在原书的基础上使用es6 class重写了一遍代码:
// 售楼处作为发布者
class SalesOffice {
constructor() {
this.clients = [];
}
listen(fn) {
this.clients.push(fn);
}
trigger() {
for(let fn of this.clients) {
fn.apply(this, arguments);
}
}
}
const office = new SalesOffice();
// 小明订阅
office.listen((price, area) => console.log(`我是小明,告诉我:价格为${price},面积为${area}`));
// 小红订阅
office.listen((price, area) => console.log(`我是小红,告诉我:价格为${price},面积为${area}`));
office.trigger(10000, 180);
office.trigger(15000, 190);
上面的实现存在缺陷:无论订阅者是否感兴趣,只要数据发生变化,都会通知给所有的订阅者。所以输出会是:
我是小明,告诉我:价格为10000,面积为180
我是小红,告诉我:价格为10000,面积为180
我是小明,告诉我:价格为15000,面积为190
我是小红,告诉我:价格为15000,面积为190
为了避免这种“垃圾短信”,需要给每个顾客打上一个interest的标签,以实现顾客只收到感兴趣的内容:
// 上面的实现存在缺陷:无论订阅者是否感兴趣,只要数据发生变化,都会通知给所有的订阅者。
// 订阅者可只收到感兴趣的通知
class SalesOffice2 {
constructor() {
this.clients = new Map();
}
listen(interest, fn) {
if(this.clients.has(interest)) this.clients.get(interest).push(fn);
else this.clients.set(interest, [fn]);
}
trigger() {
let interest = Array.prototype.shift.apply(arguments);
let arr = this.clients.get(interest);
if(arr) {
for(let fn of arr) {
fn.apply(this, arguments);
}
}
else{
console.warn(`Nobody is interested in this topic!`);
}
}
}
const office2 = new SalesOffice2();
office2.listen("area150", (price, area) => console.log(`我是小明,关心150平方大小的房子,告诉我:价格为${price},面积为${area}`));
office2.listen("area180", (price, area) => console.log(`我是小红,关心180平方大小的房子,告诉我:价格为${price},面积为${area}`));
// 大小为150的房子价格更新了
office2.trigger("area150", 300000, 150);
// 大小为180的房子价格更新了
office2.trigger("area180", 320000, 180);
这样,小明只收到了area为150的价格变动通知,小红只收到area为180的价格变动通知
我是小明,关心150平方大小的房子,告诉我:价格为300000,面积为150
我是小红,关心180平方大小的房子,告诉我:价格为320000,面积为180