九、观察者模式(Observer Patterns)
1.概念介绍
观察者模式(Observer Patterns) 也称订阅/发布(subscriber/publisher)模式,这种模式下,一个对象订阅定一个对象的特定活动,并在状态改变后获得通知。
这里的订阅者称为观察者,而被观察者称为发布者,当一个事件发生,发布者会发布通知所有订阅者,并常常以事件对象形式传递消息。
所有浏览器事件(鼠标悬停,按键等事件)都是该模式的例子。
我们还可以这么理解:这就跟我们订阅微信公众号一样,当公众号(发布者)群发一条图文消息给所有粉丝(观察者),然后所有粉丝都会接受到这篇图文消息(事件),这篇图文消息的内容是发布者自定义的(自定义事件),粉丝阅读后可能就会买买买(执行事件)。
2.观察者模式 VS 发布订阅模式
2.1观察者模式
一种一对多的依赖关系,多个观察者对象同时监听一个主题对象。这个主题对象在状态上发生变化时,会通知所有观察者对象,使它们能够自动更新自己。
2.2发布订阅模式
发布订阅模式理念和观察者模式相同,但是处理方式上不同。
在发布订阅模式中,发布者和订阅者不知道对方的存在,他们通过调度中心串联起来。
订阅者把自己想订阅的事件注册到调度中心,当该事件触发时候,发布者发布该事件到调度中心(并携带上下文),由调度中心统一调度订阅者注册到调度中心的处理代码。
2.3两者异同点
- 观察者模式中,观察者知道发布者是谁,并发布者保持对观察者进行记录。而发布订阅模式中,发布者和订阅者不知道对方的存在。它们只是通过调度中心进行通信。
- 发布订阅模式中,组件是松散耦合的,正好和观察者模式相反。
- 观察者模式大多是同步,如当事件触发,发布者就会去调用观察者的方法。而发布订阅模式大多是异步的(使用消息队列)。
- 观察者模式需要在单个应用程序地址空间中实现,而发布-订阅更像交叉应用模式。
- 尽管存在差异,但也有人说发布-订阅模式是观察者模式的变异,因为它们概念上相似。
2.4两者优缺点
相同优点:
- 都可以一对多
- 程序便于扩展
不同优点:
- 观察者模式:单向解耦,发布者不需要清楚订阅者何时何地订阅,只需要维护订阅队列,发送消息即可
- 发布订阅模式:双向解耦,发布者和订阅者都不用清楚对方,全部由订阅中心做处理
缺点:
- 如果一个被观察者和多个观察者的话,会增加维护的难度,并且会消耗很多时间。
- 如果观察者和发布者之间有循环依赖,可能会导致循环调用引起系统奔溃。
- 观察者无法得知观察的目标对象是如何发生变化,只能知道目标对象发生了变化。
- 发布订阅模式,中心任务过重,一旦崩溃,所有订阅者都会受到影响。
4.基本案例
我们平常一直使用的给DOM节点绑定事件,也是观察者模式的案例:
1. document.body.addEventListener('click', function(){
2. alert('ok');
3. },false);
4. document.body.click();
这里我们订阅了 document.body
的 click
事件,当 body
点击它就向订阅者发送消息,就会弹框 ok
。我们也可以添加很多的订阅。
4.观察者模式 案例
本案例来自 javascript 观察者模式和发布订阅模式(cangmean.me/2018/08/31/js-observer/#观察者模式)。。
1. class Dom {
2. constructor() {
3. // 订阅事件的观察者
4. this.events = {}
5. }
6.
7. /**
8. * 添加事件的观察者
9. * @param {String} event 订阅的事件
10. * @param {Function} callback 回调函数(观察者)
11. */
12. addEventListener(event, callback) {
13. if (!this.events[event]) {
14. this.events[event] = []
15. }
16. this.events[event].push(callback)
17. }
18.
19. removeEventListener(event, callback) {
20. if (!this.events[event]) {
21. return
22. }
23. const callbackList = this.events[event]
24. const index = callbackList.indexOf(callback)
25. if (index > -1) {
26. callbackList.splice(index, 1)
27. }
28. }
29.
30. /**
31. * 触发事件
32. * @param {String} event
33. */
34. fireEvent(event) {
35. if (!this.events[event]) {
36. return
37. }
38. this.events[event].forEach(callback => {
39. callback()
40. })
41. }
42. }
43.
44. const handler = () => {
45. console.log('fire click')
46. }
47. const dom = new Dom()
48.
49. dom.addEventListener('click', handler)
50. dom.addEventListener('move', function() {
51. console.log('fire click2')
52. })
53. dom.fireEvent('click')
5.发布订阅模式 案例
本案例来自 javascript 观察者模式和发布订阅模式(cangmean.me/2018/08/31/js-observer/#观察者模式)。
1. class EventChannel {
2. constructor() {
3. // 主题
4. this.subjects = {}
5. }
6.
7. hasSubject(subject) {
8. return this.subjects[subject] ? true : false
9. }
10.
11. /**
12. * 订阅的主题
13. * @param {String} subject 主题
14. * @param {Function} callback 订阅者
15. */
16. on(subject, callback) {
17. if (!this.hasSubject(subject)) {
18. this.subjects[subject] = []
19. }
20. this.subjects[subject].push(callback)
21. }
22.
23. /**
24. * 取消订阅
25. */
26. off(subject, callback) {
27. if (!this.hasSubject(subject)) {
28. return
29. }
30. const callbackList = this.subjects[subject]
31. const index = callbackList.indexOf(callback)
32. if (index > -1) {
33. callbackList.splice(index, 1)
34. }
35. }
36.
37. /**
38. * 发布主题
39. * @param {String} subject 主题
40. * @param {Argument} data 参数
41. */
42. emit(subject, ...data) {
43. if (!this.hasSubject(subject)) {
44. return
45. }
46. this.subjects[subject].forEach(callback => {
47. callback(...data)
48. })
49. }
50. }
51.
52. const channel = new EventChannel()
53.
54. channel.on('update', function(data) {
55. console.log(`update value: ${data}`)
56. })
57. channel.emit('update', 123)
参考资料
- 《JavaScript Patterns》