1.观察者模式

Observer Design Pattern:
Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
翻译成中文就是:在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知,并自动更新。

  1. // 目标对象类
  2. class Subject {
  3. constructor(){
  4. this.observers = [];
  5. }
  6. // 添加观察者
  7. add(o){
  8. this.observers.push(o);
  9. }
  10. // 移除观察者
  11. remove(o){
  12. const index = this.observers.find(f=>f===o);
  13. if(index !== -1){
  14. this.observers.splice(index,1);
  15. }
  16. }
  17. // 通知
  18. notify(...args){
  19. this.observers.forEach(observer=>{
  20. observer.update(...args);
  21. })
  22. }
  23. }
  24. // 观察者类
  25. class Observer {
  26. // 更新
  27. update(...args){
  28. console.log('目标对象的状态变化了,我需要更新',args)
  29. }
  30. }
  31. const subject = new Subject();
  32. const observer1 = new Observer();
  33. const observer2 = new Observer();
  34. subject.add(observer1);
  35. subject.add(observer2);
  36. subject.notify('new state');

2.发布订阅模式

object/array方式实现

  1. // 全局事件总线 EventEmitter
  2. // 推荐阅读FaceBook推出的通用EventEmiiter库的源码(https://github.com/facebookarchive/emitter)
  3. class EventEmitter {
  4. constructor() {
  5. // handlers是一个map,用于存储事件名与回调之间的对应关系
  6. this.handlers = {};
  7. }
  8. // 用于安装事件监听器,它接受事件名和回调函数作为参数
  9. on(eventName, cb) {
  10. if (!this.handlers[eventName]) {
  11. this.handlers[eventName] = [];
  12. }
  13. this.handlers[eventName].push(cb);
  14. }
  15. // 用于移除某个事件回调队列里的指定回调函数
  16. off(eventName, cb) {
  17. if (!this.handlers[eventName]) {
  18. return;
  19. }
  20. const index = this.handlers[eventName].findIndex(f => f === cb);
  21. this.handlers[eventName].splice(index, 1);
  22. }
  23. // 用于触发目标事件,它接受事件名和监听函数入参作为参数
  24. emit(eventName, ...args) {
  25. if (!this.handlers[eventName]) {
  26. return;
  27. }
  28. this.handlers[eventName].forEach((cb) => {
  29. cb(...args);
  30. });
  31. }
  32. // 为事件注册单次监听器
  33. once(eventName, cb) {
  34. // 对回调函数进行包装,使其执行完毕后自动被移除
  35. const wrapper = (...args) => {
  36. cb.apply(null, args);
  37. this.off(eventName, wrapper);
  38. };
  39. this.on(eventName, wrapper);
  40. }
  41. }

map/set方式实现

  1. class EventEmitter {
  2. constructor() {
  3. // handlers是一个map,用于存储事件名与回调之间的对应关系
  4. this.handlers = new Map();
  5. }
  6. // 用于安装事件监听器,它接受事件名和回调函数作为参数
  7. on(eventName, cb) {
  8. if (!this.handlers.has(eventName)) {
  9. this.handlers.set(eventName, new Set());
  10. }
  11. this.handlers.get(eventName).add(cb);
  12. }
  13. // 用于移除某个事件回调队列里的指定回调函数
  14. off(eventName, cb) {
  15. if (!this.handlers.has(eventName)) {
  16. return;
  17. }
  18. this.handlers.get(eventName).delete(cb);
  19. }
  20. // 用于触发目标事件,它接受事件名和监听函数入参作为参数
  21. emit(eventName, ...args) {
  22. if (!this.handlers.has(eventName)) {
  23. return;
  24. }
  25. this.handlers.get(eventName).forEach((cb) => {
  26. cb(...args);
  27. });
  28. }
  29. // 为事件注册单次监听器
  30. once(eventName, cb) {
  31. // 对回调函数进行包装,使其执行完毕后自动被移除
  32. const wrapper = (...args) => {
  33. cb.call(null, ...args);
  34. this.off(eventName, wrapper);
  35. };
  36. this.on(eventName, wrapper);
  37. }
  38. }

案例

在公众号消息卡片跳转小程序的某个详情页的时候,比如此时认证信息过期或者用户没有登录过,获取详情数据的接口调用失败,此时会重定向到登录页,登录成功后,在跳转回详情页,但是详情页已经加载完成,因此不会再重新发请求获取详情信息,如何解决?
我们的解决方案是在详情页初始化时,订阅一个登录成功的事件,对应的回调函数是获取详情页数据,然后在登录成功的时候,触发登录成功的事件。

3.两者区别

  • 对于观察者模式,一定是有两个角色(即目标对象类和观察者类),目标对象和观察者是直接产生联系的,因为观察者就在目标对象的数组里,这样才能保证当目标对象的状态发生变化时,能够调用所有观察者的update方法。目标对象和观察者耦合在了一起。
  • 而发布订阅模式,只有一个角色(即第三方),已经没有发布者和订阅者了,完全由这个第三方完成所有的功能,它是事件名和回调函数之间的对应关系。