前言

这是关于设计模式的系列文章,在每篇文章中将对常见设计模式进行讲解,因为针对前端方向,而且前端常用语言 JavaScript 本身是弱类型,面向对象(模拟面向对象)编程的实现相较于其他强类型语言实现更为繁琐,所以代码主要以 JavaScript 表现。

系列文章链接:

发布/订阅模式

发布/订阅模式实现

在说 “观察者模式” 之前一定要说一下 “发布/订阅模式”,因为这两个模式非常相似又有些不同,最重要的是在设计模式中使用频繁。

/* 发布/订阅的类 */
class Event {
  constructor() {
    this.events = {};
  }
  on(type, fn) {
    (this.events[type] || (this.events[type] = [])).push(fn)
  }
  emit(type) {
    if (this.events[type]) {
      this.events[type].forEach(fn => fn());
    }
  }
}

上面的类有一个基本属性 events,值为对象,用来存储不同类型的事件集合,原型方法 on 是用来订阅事件,第一个参数 type 为订阅事件的类型,fn 是要被执行的事件,emit 方法用来执行某个类型所有的事件。

/* 发布/订阅的使用 */
const event = new Event();

// 订阅事件
event.on('say', () => console.log('hello'));
event.on('say', () => console.log('world'));

// 发布事件
event.emit('say');

// hello
// world

发布/订阅模式的应用

浏览器事件监听:

<button id="btn">click</button>
<script>
  const btn = document.getElementById('btn');

  btn.addEventListener('click', () => console.log(1));
  btn.addEventListener('click', () => console.log(2));
  btn.addEventListener('click', () => console.log(3));
</script>

当点击按钮触发事件时,三个回调函数会按照添加的顺序依次执行。

Promise 的异步调用的回调管理:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => resolve('res'), 3000);
});

promise.then(data => console.log(1, data));
promise.then(data => console.log(2, data));

上面给同一个 Promise 实例的 then 方法中添加了两个回调函数,因为在创建 Promise 实例时内部使用了定时器,所以状态的变化延后了 3s,其实在 Promise 内部也是通过队列来对 then 的回调进行统一管理,在状态发生变化后立即循环执行。

Node.js 中,有一个核心模块 events 提供的类 EventEmitter,几乎所有的事件都是基于这个模块实现的,如流的 dataend 事件、httprequest 事件,而 EventEmitter 的内部原理就是 “发布/订阅模式”。

观察者模式

观察者模式的简单实现

下面我们是 “观察者模式” 的案例,通过上面 “发布/订阅模式” 的实现来对比一下异同。

/* 观察者模式 */
// 被观察者类
class Star {
  constructor(name) {
    this.name = name;
    this.state = '';
    this.observers = [];
  }
  getState() {
    return this.state;
  }
  setState(state) {
    this.state = state;
    this.notify(); // 更新状态后通知
  }
  attach(observer) {
    this.observers.push(observer); // 添加观察者
  }
  notify() {
    // 订阅状态的观察者更新修改后的状态
    this.observers.forEach(observer => observer.update());
  }
}

// 观察者类
class Fan {
  constructor(name, star) {
    this.name = name;
    this.star = star;
    this.star.attach(this);
  }
  update() {
    console.log('我喜欢的明星喜欢' + this.star.getState() + ',我也喜欢。');
  }
}

在 “观察者模式” 中有两个基本的类,观察者和被观察者,被观察者提供状态 state,观察者去使用这个状态,当被观察者更新状态时会主动发布到订阅了状态的观察中,实现同步更新。

const star = new Star('Super Star');
const fan = new Fan('张三', star);

star.setState('绿色');

// 我喜欢的明星喜欢绿色,我也喜欢。

观察者模式的应用

Vue 框架的数据响应式原理及 watch 方法:

<div id="root">
  <p>FullName: {{fullName}}</p>
  <p>
    FirstName:
    <input type="text" v-model="firstName" />
  </p>
  <p>
    LastName:
    <input type="text" v-model="lastName" />
  </p>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>
  new Vue({
    el: '#root',
    data: {
      firstName: '张',
      lastName: '三',
      fullName: '张三'
    },
    watch: {
      firstName(newName, oldName) {
        this.fullName = newName + this.lastName;
      }
    }
  });
</script>

Vue 2.x 版本的数据响应式和 watch 监听的底层,就使用了 “观察者模式”,在模板解析过程中为变量添加观察者(watcher),在使用 Object.definePropertygettersetter 进行劫持数据,数据获取和更改会触发 getset 方法,进而执行订阅和通知的逻辑,而通知的过程中调用了被统一管理的观察者的 update 方法,实现了视图层与数据层的同步。

reduxcreateStore 方法:

function createStore(reducer) {
  let state;
  let listeners = [];

  // 获取 store
  const getState = () => JSON.parse(JSON.stringify(state));

  // 订阅方法
  const subscribe = fn => {
    listeners.push(fn);

    // 取消订阅方法
    return () => {
      listeners = listeners.filter(listener => listener !== fn);
    };
  }

  // 派发动作
  const dispatch = action => {
    state = reducer(state, action);
    listeners.forEach(listener => listener());
  }

  dispatch({ type: '@INIT' });

  return {
    getState,
    subscribe,
    dispatch
  };
}

redux 的实现思想中也包含了 “观察者模式”,例如在 reduxReact 的配合使用,redux 提供了订阅的方法 subscribe 和派发动作更新 store 的方法 dispatchReact 组件会使用 store 中提供的状态数据,这个 store 就是被观察者,而观察者就是 React 的各个组件,当使用 dispatch 派发动作更新数据时,会执行所有的观察者中的监听函数,实现组件数据与 store 的同步。

发布/订阅模式和观察者模式

观察者模式是由 “发布/订阅模式” 演变过来的,都存在订阅、通知的事件机制,“发布/订阅模式” 是对订阅的事件进行统一管理,主动触发通知,依次执行订阅的事件,而 “观察者模式” 是通过一个单独类去订阅观察者,当状态发生变化时通知到各个 “观察者” 实现状态的更新同步。

“发布/订阅模式” 与 “观察者模式” 的区别:

  • “发布/订阅模式” 事件是统一由调度中心调度,订阅发布不存在依赖;
  • “观察者模式” 事件是被观察者调度,订阅与发布是存在依赖的;

总结

“观察者模式” 的意义就在于可以使多个对象数据重合的部分进行复用,同时还可以对各个对象之间解耦,最重要的是数据更新可以及时通知所有数据的使用者进行数据同步,最后附上 案例地址