观察者模式,是JavaScript设计模式之一。当然也不仅仅限于JavaScript这门语言,网上对该模式的介绍已是多如牛毛,而且讲得各有特色各有心得。即便如此,笔者仍精心准备了这篇博客,期望用最简单的方式来介绍下该模式。
首先来看下维基百科对 观察者模式 的解释:
观察者模式是软件设计模式的一种。在此种模式中,一个目标对象管理所有相依于它的观察者对象,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。此种模式通常被用来实时事件处理系统。
其实笔者更倾向于它的另一个名字发布/订阅模式(Publish/Subscribe),因为更能表达出该模式的核心思路,那就是:发布和订阅两个过程。是不是还感觉模棱两可?不用担心,下面就用咱们身边发生的事情来做个形象化的解释:
大家都有订阅网站邮件的经历吧?如果你没有的话,emmmmm....那就继续往下看吧哈哈!!
假如我今天想订阅xxx公司的邮件,那么这里就涉及到两个对象:我
和xxx公司
,
从行为上来看就是我订阅
了xxx公司邮件,xxx公司会发送
邮件给我的邮箱。但某天我不想再收到xxx公司的邮件了,那么我可以取消订阅
,这样xxx公司就不会再发邮件到我的邮箱。
说到这里,是不是就有点眉头了呢?好,我们继续往下说,
通过刚刚的形象化解释,我们可以罗列下观察者模式的一些核心的东西:
对象:我(订阅者)
, xxx公司(发布者)
, 可以直接对应 发布/订阅模式(Publish/Subscribe)
行为:订阅
、发送
和取消订阅
说不如做,下面开始用代码来更直观的描绘下观察者模式吧。
首先我们定义一个发布者 (相当于xxx公司)
let publisher = {
}
那么一起来按照订阅邮件的过程想象下,发布者具有那些属性或者方法?
- 首先,我们订阅一个xxx网站的邮件,是不是需要xxx网站给我们提供订阅入口?那么
publisher
中必定会有一个方法提供给我们实现订阅
。 - 其次,如果xxx公司要向订阅者们发送自己的邮件,是不是需要一个方法去做?那么
publisher
中必定会有一个方法提供给我们实现发送
或者说说发布
。 - 再然后,如例子所说如果我突然不想订阅xxx公司的邮件了,xxx公司 就得提供给我一个取消订阅的入口,那么
publisher
中必定会有一个方法提供给我们实现取消订阅
。 - 最后,如果我们订阅了xxx公司的邮件,那么他就得记录我订阅所用的邮箱地址吧,所以
publisher
中必定会有一个“注册表”
来存储订阅的对象,也就是说我们的 邮箱地址。
说到这里一切都了然了,下面还是讲想象到的东西用代码表达出来吧
let publisher = {
registration: {},
subscribe: function (type, fn) {},
unSubscribe: function (type, fnName) {},
publish: function (type, message) {}
}
简单解释下,
-
registration
就是上面提到的注册表
,至于为什么把它设计成一个对象是因为考虑到xxx公司可能有更多类型的邮件,比如 游戏,金融,投资理财等等,所以就把它设计成对象以key-value的形式存储订阅者, 比如:{'game':[],'monetary':[]}
该形式 -
subscribe
则是publisher
提供给我们的对其进行订阅的方法,参数是type
和fn
。type就是邮件的类型,fn就是我们提供给publisher用于通知我的渠道 (邮箱)。在JavaScript中更多的是回调函数。 -
unSubscribe
是publisher
提供给我们的对其进行取消订阅的方法,参数是type
和fnName
。type就不多说了,fnName则是我们提供给publisher
用于取消订阅的标志,比如说邮箱,或者是回调函数的名字等等。 -
publish
说到比较重要的方法,这就是publisher
向所有订阅者发布消息的方法。
下面开始一步一步得实现三个方法,registration
保持不变:
首先是subscribe
subscribe: function (type, fn) {
if (Object.keys(this.registration).indexOf(type) >= 0) {
this.registration[type].push(fn);
} else {
this.registration[type] = [];
this.registration[type].push(fn);
}
}
这里的思路是将 Callback Function 存储到registration
对于类型的数组中,以待publish
调用。
然后是 unSubscribe
unSubscribe: function (type, fnName) {
if (Object.keys(this.registration).indexOf(type) >= 0) {
let index = -1;
this.registration[type].forEach(function (func, idx) {
if (func.name === fnName) {
index = idx;
}
})
index > -1 ? this.registration[type].splice(index, 1) : null
}
}
思路是首先通过 type 确定数组对象,然后通过方法对象的名字进行判断,最后直接剔除操作。** 这里有个小知识点提一下:函数对象的name属性就是该函数名 **
最后是 publish
publish: function (type, message) {
if (Object.keys(this.registration).indexOf(type) >= 0) {
for (let fn of this.registration[type]) {
fn(message)
}
}
}
思路是通过 type 找到指定数组,然后对数组中的回调函数进行依次调用,达到发布
的目的。
写到这里,发布者Publisher
已经完成。那么下面开始写订阅者Subscriber
,如上面所说其实订阅者就是一个 回调函数,例如:
let subscriber = function (param) {
//do something
}
所以下面将整个代码展示并演示下效果:
let publisher = {
registration: {},
subscribe: function (type, fn) {
if (Object.keys(this.registration).indexOf(type) >= 0) {
this.registration[type].push(fn);
} else {
this.registration[type] = [];
this.registration[type].push(fn);
}
},
unSubscribe: function (type, fnName) {
if (Object.keys(this.registration).indexOf(type) >= 0) {
let index = -1;
this.registration[type].forEach(function (func, idx) {
if (func.name === fnName) {
index = idx;
}
})
index > -1 ? this.registration[type].splice(index, 1) : null
}
},
publish: function (type, message) {
if (Object.keys(this.registration).indexOf(type) >= 0) {
for (let fn of this.registration[type]) {
fn(message)
}
}
}
}
let subscriberA = function (message) {
console.log(`A收到通知:${message}`)
};
let subscriberB = function (message) {
console.log(`B收到通知:${message}`)
};
let subscriberC = function (message) {
console.log(`C收到通知:${message}`)
};
publisher.subscribe('game', subscriberA);
publisher.subscribe('game', subscriberB);
publisher.subscribe('game', subscriberC);
publisher.publish('game', '恭喜RNG获得LOL 2018季中赛冠军!')
运行看下结果:
结果如想象中一样。
那再试一下取消订阅
,在 publish 之前加一段
publisher.unSubscribe('game', subscriberB.name)
再运行看下结果:
我们已经看到 订阅者B 在取消订阅后就没再收到任何消息。
其实观察者模式能做的东西还有很多,比如事件的监听、状态发生变化时的广播等等。已经有过接触的朋友都可能意识到这个模式特别灵活,在两个角色之间正常通信的同时也尽可能得实现了解耦,给开发带来极大的便利。其中有名的 Knockout 的核心之一就是观察者模式,所以说观察者模式在前端开发中起到了举足轻重的作用。
源码在这,有兴趣的朋友可以看下
好了,写到这里本篇博客就结束了。有问题的朋友可以在下方讨论;如果文章有不足或者错误的地方,烦请大家多多指正。Thanks !!!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。