您的当前位置:首页正文

JavaScript 设计模式(六):观察者模式与发布订阅模式

来源:要发发知识网

观察者模式(Observer)

观察者模式:定义了对象间一种一对多的依赖关系,当目标对象 Subject 的状态发生改变时,所有依赖它的对象 Observer 都会得到通知。

简单点:女神有男朋友了,朋友圈晒个图,甜蜜宣言 “老娘成功脱单,希望你们欢喜”。各位潜藏备胎纷纷失恋,只能安慰自己你不是唯一一个。

模式特征

  1. 一个目标者对象 Subject,拥有方法:添加 / 删除 / 通知 Observer

  2. 多个观察者对象 Observer,拥有方法:接收 Subject 状态变更通知并处理;

  3. 目标对象 Subject 状态变更时,通知所有 Observer

Subject 添加一系列 ObserverSubject 负责维护与这些 Observer 之间的联系,“你对我有兴趣,我更新就会通知你”。

代码实现

// 目标者类
class Subject {
  constructor() {
    this.observers = [];  // 观察者列表
  }
  // 添加
  add(observer) {
    this.observers.push(observer);
  }
  // 删除
  remove(observer) {
    let idx = this.observers.findIndex(item => item === observer);
    idx > -1 && this.observers.splice(idx, 1);
  }
  // 通知
  notify() {
    for (let observer of this.observers) {
      observer.update();
    }
  }
}

// 观察者类
class Observer {
  constructor(name) {
    this.name = name;
  }
  // 目标对象更新时触发的回调
  update() {
    console.log(`目标者通知我更新了,我是:${this.name}`);
  }
}

// 实例化目标者
let subject = new Subject();

// 实例化两个观察者
let obs1 = new Observer('前端开发者');
let obs2 = new Observer('后端开发者');

// 向目标者添加观察者
subject.add(obs1);
subject.add(obs2);

// 目标者通知更新
subject.notify();  
// 输出:
// 目标者通知我更新了,我是前端开发者
// 目标者通知我更新了,我是后端开发者

优势

  1. 目标者与观察者,功能耦合度降低,专注自身功能逻辑;

  2. 观察者被动接收更新,时间上解耦,实时接收目标者更新状态。

不完美

观察者模式虽然实现了对象间依赖关系的低耦合,但却不能对事件通知进行细分管控,如 “筛选通知”,“指定主题事件通知” 。

比如上面的例子,仅通知 “前端开发者” ?观察者对象如何只接收自己需要的更新通知?上例中,两个观察者接收目标者状态变更通知后,都执行了 update(),并无区分。

“00后都在追求个性的时代,我能不能有点不一样?”,这就引出我们的下一个模式。进阶版的观察者模式。“发布订阅模式”,部分文章对两者是否一样都存在争议。

仅代表个人观点:两种模式很类似,但是还是略有不同,就是多了个第三者,因 JavaScript 非正规面向对象语言,且函数回调编程的特点,使得 “发布订阅模式” 在 JavaScript 中代码实现可等同为 “观察模式”。

发布订阅模式(Publisher && Subscriber)

发布订阅模式:基于一个事件(主题)通道,希望接收通知的对象 Subscriber 通过自定义事件订阅主题,被激活事件的对象 Publisher 通过发布主题事件的方式通知各个订阅该主题的 Subscriber 对象。

发布订阅模式与观察者模式的不同,“第三者” (事件中心)出现。目标对象并不直接通知观察者,而是通过事件中心来派发通知。

代码实现

// 事件中心
let pubSub = {
  list: {},
  subscribe: function (key, fn) {   // 订阅
    if (!this.list[key]) {
      this.list[key] = [];
    }
    this.list[key].push(fn);
  },
  publish: function(key, ...arg) {  // 发布
    for(let fn of this.list[key]) {
      fn.call(this, ...arg);
    }
  },
  unSubscribe: function (key, fn) {     // 取消订阅
    let fnList = this.list[key];
    if (!fnList) return false;

    if (!fn) {
      // 不传入指定取消的订阅方法,则清空所有key下的订阅
      fnList && (fnList.length = 0);
    } else {
      fnList.forEach((item, index) => {
        if (item === fn) {
          fnList.splice(index, 1);
        }
      })
    }
  }
}

// 订阅
pubSub.subscribe('onwork', time => {
  console.log(`上班了:${time}`);
})
pubSub.subscribe('offwork', time => {
  console.log(`下班了:${time}`);
})
pubSub.subscribe('launch', time => {
  console.log(`吃饭了:${time}`);
})

// 发布
pubSub.publish('offwork', '18:00:00'); 
pubSub.publish('launch', '12:00:00');

// 取消订阅
pubSub.unSubscribe('onwork');

发布订阅模式中,订阅者各自实现不同的逻辑,且只接收自己对应的事件通知。实现你想要的 “不一样”。

DOM 事件监听也是 “发布订阅模式” 的应用:

let loginBtn = document.getElementById('#loginBtn');

// 监听回调函数(指定事件)
function notifyClick() {
    console.log('我被点击了');
}

// 添加事件监听
loginBtn.addEventListener('click', notifyClick);
// 触发点击, 事件中心派发指定事件
loginBtn.click();             

// 取消事件监听
loginBtn.removeEventListener('click', notifyClick);

发布订阅的通知顺序:

  1. 先订阅后发布时才通知(常规)

  2. 订阅后可获取过往以后的发布通知 (QQ离线消息,上线后获取之前的信息)

流行库的应用

  1. jQuery 的 ontrigger$.callback();

  2. Vue 的双向数据绑定;

  3. Vue 的父子组件通信 $on/$emit

jQuery 的 $.Callback()

jQuery 的 $.Callback() 更像是观察者模式的应用,不能更细粒度管控。

function notifyHim(value) {
 console.log('He say ' + value);
}

function notifyHer(value) {
 console.log('She say ' + value);
}

$cb = $.Callbacks();    // 声明一个回调容器:订阅列表 

$cb.add(notifyHim);     // 向回调列表添加回调:订阅
$cb.add(notifyHer);     // 向回调列表添加回调:订阅

$cb.fire('help');       // 调用所有回调: 发布
Vue 的双向数据绑定
Vue双向数据绑定

利用 Object.defineProperty() 对数据进行劫持,设置一个监听器 Observer,用来监听数据对象的属性,如果属性上发生变化了,交由 Dep 通知订阅者 Watcher 去更新数据,最后指令解析器 Compile 解析对应的指令,进而会执行对应的更新函数,从而更新视图,实现了双向绑定。

  1. Observer (数据劫持)
  2. Dep (发布订阅)
  3. Watcher (数据监听)
  4. Compile (模版编译)
Vue 的父子组件通信

Vue 中,父组件通过 props 向子组件传递数据(自上而下的单向数据流)。父子组件之间的通信,通过自定义事件即 $on , $emit 来实现(子组件 $emit,父组件 $on)。

原理其实就是 $emit 发布更新通知,而 $on 订阅接收通知。Vue 中还实现了 $once(一次监听),$off(取消订阅)。

// 订阅
vm.$on('test', function (msg) {
    console.log(msg)
})

// 发布
vm.$emit('test', 'hi')

优势

  1. 对象间功能解耦,弱化对象间的引用关系;
  2. 更细粒度地管控,分发指定订阅主题通知

不完美

  1. 对间间解耦后,代码阅读不够直观,不易维护;
  2. 额外对象创建,消耗时间和内存(很多设计模式的通病)

观察者模式 VS 发布订阅模式

观察者模式 VS 发布订阅模式

类似点

都是定义一个一对多的依赖关系,有关状态发生变更时执行相应的通知。

区别点

发布订阅模式更灵活,是进阶版的观察者模式,指定对应分发。

  1. 观察者模式维护单一事件对应多个依赖该事件的对象关系;

  2. 发布订阅维护多个事件(主题)及依赖各事件(主题)的对象之间的关系;

  3. 观察者模式是目标对象直接触发通知(全部通知),观察对象被迫接收通知。发布订阅模式多了个中间层(事件中心),由其去管理通知广播(只通知订阅对应事件的对象);

  4. 观察者模式对象间依赖关系较强,发布订阅模式中对象之间实现真正的解耦。


对象属性数据拦截方式:

  1. Object.defineProperty() 属性描述符;
  2. ES6 Class set ;
  3. ES6 Proxy 代理;

参考文章:

作者:以乐之名
本文原创,有不当的地方欢迎指出。转载请指明出处。