Published on

观察者模式详解

Authors
  • avatar
    Name
    青雲
    Twitter

观察者模式(Observer Pattern)是一种行为型设计模式,它定义了一种一对多的依赖关系,当一个对象的状态发生变化时,其所有依赖者(观察者)都会收到通知并自动更新。观察者模式广泛应用于各种场景,例如事件处理系统、订阅通知系统和MVC架构等。

为什么需要观察者模式?

生活中,你订阅了一家报纸(被观察者)。报社有很多像你这样的订阅者(观察者),他们可能对体育新闻、财经新闻或其他类型的新闻感兴趣。每当报社出版了新的报纸时,它就会按订阅者的喜好,将包含相关内容的报纸送到每个订阅者手中。 在这个场景中:

  • 报社相当于是被观察者。它知道哪些人订阅了报纸,并负责在有新内容时通知他们。
  • 订阅者则是观察者。他们只关心自己感兴趣的内容,并依赖报社提供这些内容。

每当报社发布新的报纸时(被观察者状态改变),所有的订阅者(观察者)都会收到他们感兴趣的新闻部分的通知。 在软件开发中,某些对象需要在其他对象发生变化时得到通知并作出响应。例如,GUI 应用中的按钮点击事件、实时数据的更新等。如果让观察对象直接通知每个依赖者,会导致耦合性增加,系统难以维护和扩展。

通过观察者模式,可以解耦观察对象和依赖对象,使得观察者模式成为监听变化、通知更新的理想选择。

基本概念

观察者模式包含以下角色:

  1. 主题(Subject):管理所有观察者,并在自身状态发生变化时通知所有观察者。
  2. 观察者(Observer):定义一个接口,用于接收通知。
  3. 具体主题(Concrete Subject):具体实现主题接口,维护一个观察者列表,在状态发生变化时通知所有观察者。
  4. 具体观察者(Concrete Observer):具体实现观察者接口,在接收到通知时进行相应的更新操作。

实现示例

假设我们要实现一个天气预报系统,气象站(主题)会通知多个显示板(观察者)天气信息。当气象站更新天气信息时,所有的显示板都会自动更新并显示新的信息。

定义主题和观察者接口

// 观察者接口
interface Observer {
  update(temperature: number, humidity: number, pressure: number): void;
}

// 主题接口
interface Subject {
  registerObserver(observer: Observer): void;
  removeObserver(observer: Observer): void;
  notifyObservers(): void;
}

实现具体主题

// 具体主题类,管理观察者并通知他们
class WeatherStation implements Subject {
  private observers: Observer[] = [];
  private temperature: number;
  private humidity: number;
  private pressure: number;

  registerObserver(observer: Observer): void {
    this.observers.push(observer);
  }

  removeObserver(observer: Observer): void {
    const index = this.observers.indexOf(observer);
    if (index !== -1) {
      this.observers.splice(index, 1);
    }
  }

  notifyObservers(): void {
    for (const observer of this.observers) {
      observer.update(this.temperature, this.humidity, this.pressure);
    }
  }

  setMeasurements(temperature: number, humidity: number, pressure: number): void {
    this.temperature = temperature;
    this.humidity = humidity;
    this.pressure = pressure;
    this.notifyObservers();
  }
}

实现具体观察者

// 具体观察者类,显示天气信息
class CurrentConditionsDisplay implements Observer {
  private temperature: number;
  private humidity: number;

  update(temperature: number, humidity: number, pressure: number): void {
    this.temperature = temperature;
    this.humidity = humidity;
    this.display();
  }

  display(): void {
    console.log(`Current conditions: ${this.temperature}°C and ${this.humidity}% humidity`);
  }
}

class StatisticsDisplay implements Observer {
  private maxTemp: number = 0;
  private minTemp: number = 100;
  private tempSum: number = 0;
  private numReadings: number = 0;

  update(temperature: number, humidity: number, pressure: number): void {
    this.tempSum += temperature;
    this.numReadings++;

    if (temperature > this.maxTemp) {
      this.maxTemp = temperature;
    }

    if (temperature < this.minTemp) {
      this.minTemp = temperature;
    }

    this.display();
  }

  display(): void {
    console.log(`Avg/Max/Min temperature = ${(this.tempSum / this.numReadings).toFixed(2)}°C / ${this.maxTemp}°C / ${this.minTemp}°C`);
  }
}

使用观察者模式

// 创建气象站和显示板
const weatherStation = new WeatherStation();
const currentDisplay = new CurrentConditionsDisplay();
const statsDisplay = new StatisticsDisplay();

// 注册观察者
weatherStation.registerObserver(currentDisplay);
weatherStation.registerObserver(statsDisplay);

// 更新天气信息
weatherStation.setMeasurements(25, 65, 1013);
weatherStation.setMeasurements(27, 70, 997);
weatherStation.setMeasurements(22, 90, 1002);
Current conditions: 25°C and 65% humidity
Avg/Max/Min temperature = 25.00°C / 25°C / 25°C
Current conditions: 27°C and 70% humidity
Avg/Max/Min temperature = 26.00°C / 27°C / 25°C
Current conditions: 22°C and 90% humidity
Avg/Max/Min temperature = 24.67°C / 27°C / 22°C

应用场景

事件监听及处理

观察者模式的核心思想非常适合事件监听和处理的场景。例如,当用户点击按钮、输入文字或进行滑动操作时,都会触发相应的事件。开发者可以通过添加事件监听器来观察这些事件,一旦事件发生,相应的回调函数就会被执行。

// HTML元素作为Subject,addEventListener作为订阅操作,回调函数作为Observer
document.getElementById('myButton').addEventListener('click', function() {
  console.log('Button was clicked.');
});
  • 通过 addEventListener 方法将回调函数(观察者)添加到按钮(HTML 元素)的点击事件(主题)上。
  • 当按钮被点击时,回调函数会被执行,打印出 "Button was clicked."。

MVC 或 MVVM 架构

在 MVC 或 MVVM 架构中,模型(Model)的变化通常需要及时反映到视图(View)上。这时,可以通过观察者模式让视图观察模型的状态,一旦模型状态发生变化,通知视图进行相应更新。

在前端框架中,Vue.js 是 MVVM 架构的经典应用,它通过视图模型(ViewModel)观察模型(Model)的变化,从而实现视图(View)的自动更新。Vue.js 利用实现了观察者模式的变种 (基于Object.defineProperty或者Proxy),来响应式地连接数据和视图。

Vue.js 响应式系统的核心机制如下:

  1. 数据劫持:通过 Observer 类将对象的每个属性转化为 gettersetter,在数据被访问或修改时进行拦截。
  2. 依赖收集:通过 Dep 类收集依赖,当数据变化时,通知这些依赖进行更新。
  3. 单向数据流:通过 Watcher 类订阅属性变化,数据变化时,通知所有依赖进行视图更新。

MobX

MobX 是一个功能强大的状态管理库,它利用观察者模式来实现响应式的状态管理,使得状态的变化能够自动通知并更新相关的观察者组件。MobX 通过 Actions、State、Computed Values 和 Reactions 这几个核心概念来实现其功能。

核心概念

  1. Actions :用于修改状态(State)以及产生副作用的函数,是唯一可以修改状态的方式。
  2. State :被定义为可观察状态(Observable State),可以是原始值、对象、数组、引用等。
  3. Computed Values :从状态派生出来的值,自动缓存,当且仅当依赖的状态发生改变时才重新计算。
  4. Reactions :类似于 Computed Values,但不产生新值,而是产生副作用(如更新 UI)。

工作流程

image.png

定义 Actions

Actions 是唯一可以修改状态的方法。它们可以包含其他的副作用,如网络请求、日志记录等。

import { observable, action } from 'mobx';

class TodoStore {
  @observable todos = [{ title: 'Learn MobX', done: false }];
  @action
  onClick = () => {
    this.todos[0].done = true;
  };
}

定义 Observable State

State 是可观察的状态,只有被定义为 observable 的状态才会被 MobX 追踪。

import { observable } from 'mobx';

class TodoStore {
  @observable todos = [
    { title: 'Learn MobX', done: false }
  ];
}

定义 Computed Values

Computed Values 是从状态中派生出来的纯函数值,只在依赖的状态变化时重新计算,使用时会自动更新。

import { observable, computed } from 'mobx';

class TodoStore {
  @observable todos = [
    { title: 'Learn MobX', done: false }
  ];
  @computed
  get completedTodos() {
    return this.todos.filter(todo => todo.done);
  }
}

使用 Reactions 更新 UI

Reactions 类似于 Computed Values,但它不产生新值,而是产生副作用,比如更新 UI。

import React from 'react';
import { observer } from 'mobx-react';

const Todos = observer(({ store }) => (
  <ul>
    {store.todos.map(todo => (
      <TodoView todo={todo} key={todo.title} />
    ))}
  </ul>
));

const TodoView = observer(({ todo }) => (
  <li>{todo.title} {todo.done && '✓'}</li>
));

// 使用示例
import ReactDOM from 'react-dom';
import { observable } from 'mobx';

const store = new TodoStore();
ReactDOM.render(<Todos store={store} />, document.getElementById('root'));

MobX 响应式系统核心机制

  1. observable:使状态变得可观察,能够追踪状态的变化。
  2. computed:定义从状态派生的值,缓存值以获得更好的性能。
  3. action:唯一允许修改状态的方法,确保状态的变更是明确且可追踪的。
  4. autorunreaction:自动触发的副作用,监听 observable 状态的变化并执行相应的逻辑。

RxJS

RxJS是一个基于观察者模式的库,它提供了一组 API 用于处理事件流和异步数据流。在 RxJS 中,Observable(可观察对象)作为事件或数据流源,Observer(观察者)订阅这些流并对事件作出反应。

核心概念

  1. Observable:可观察对象,代表一个事件流或数据流源。
  2. Observer:观察者,对 Observable 发出的数据或事件作出反应。
  3. Subscription:订阅,将 Observer 订阅到 Observable 上。
  4. Operators:操作符,用于转换和处理 Observable 中的数据流。
  5. Subject:既是 Observable 又是 Observer,通常用来实现多播或事件总线。
import { Observable, Subject, fromEvent } from 'rxjs';
import { map, filter, debounceTime } from 'rxjs/operators';

// 创建一个简单的 Observable
const observable = new Observable<number>(subscriber => {
  let count = 0;
  const intervalId = setInterval(() => {
    subscriber.next(count++);
  }, 1000);

  // 清理操作
  return () => {
    clearInterval(intervalId);
  };
});

// 创建一个观察者
const observer = {
  next: (value: number) => console.log('Received value:', value),
  error: (err: any) => console.error('Error:', err),
  complete: () => console.log('Complete')
};

// 订阅观察者到 Observable
const subscription = observable.subscribe(observer);

// 在5秒后取消订阅
setTimeout(() => {
  subscription.unsubscribe();
  console.log('Unsubscribed from observable');
}, 5000);
  1. Observable:创建了一个每秒发出一个值的 Observable。
  2. Observer:定义了一个观察者对象,对接收到的值、错误和完成事件作出反应。
  3. Subscription:将观察者订阅到 Observable,并在 5 秒后取消订阅。

如果想要对 RxJS 有更为详尽的了解,不妨查看我之前所创作的 RxJS 系列文章。

WebSocket 实时数据推送

在使用 WebSocket 实现实时通信场景中,Web 应用(观察者)会订阅一个 WebSocket 服务(主题),一旦服务端有数据推送过来,Web 应用就会接收到这些数据并进行相应处理。

// 创建 WebSocket 连接
const socket = new WebSocket('ws://example.com/ws');

// 订阅消息事件
socket.onmessage = function(event) {
  console.log('Received data:', event.data);
};

// 发送消息
socket.send('Hello Server');
  • WebSocket 连接作为主题(Subject),onmessage 事件处理函数作为观察者。
  • 当 WebSocket 服务端推送数据时,观察者会被通知并接收数据。

自定义事件系统

前端应用中,尤其是复杂的单页应用(SPA),往往需要构建一个自定义事件系统来跨组件通信。通过创建一个事件中心,组件可以向其注册监听自己感兴趣的事件,也可以触发事件来通知其他组件。

// 定义事件中心
class EventCenter {
  constructor() {
    this.listeners = {};
  }

  on(event, listener) {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event].push(listener);
  }

  emit(event, data) {
    if (this.listeners[event]) {
      this.listeners[event].forEach(listener => listener(data));
    }
  }
}

// 使用事件中心
const eventCenter = new EventCenter();

// 注册事件监听器
eventCenter.on('loginSuccess', function(userInfo) {
  console.log('User info on login:', userInfo);
});

// 触发事件
eventCenter.emit('loginSuccess', { username: 'john_doe' });
  • EventCenter 类作为自定义事件中心,实现了事件的订阅和触发功能。
  • 组件可以向 EventCenter 注册监听事件,也可以通过它触发事件,通知其他组件。

优缺点

优点

  1. 解耦:观察者模式将观察对象和观察者解耦,使得对象可以独立地变化。
  2. 灵活性:观察者可以在运行时动态加入和解除,使得系统更加灵活和可扩展。
  3. 符合开闭原则:能够在不修改主题和观察者类的前提下增加新的观察者。

缺点

  1. 可能引起性能问题:大量观察者可能导致通知开销增大,影响性能。
  2. 反应顺序:观察者的反应顺序可能难以掌控,不同观察者之间的依赖性可能引发复杂的更新。

总结

观察者模式通过定义一种一对多的依赖关系,实现了对象之间的松耦合,以便当一个对象的状态发生变化时,其所有依赖者(观察者)都会收到通知并自动更新。在前端开发中,观察者模式有着广泛的应用场景,并被众多流行框架和库所采用。

应用场景:

  1. 事件监听及处理:为用户交互事件添加监听器,当事件触发时执行相应的回调函数。例如,用户点击按钮时的事件处理。
  2. 模型-视图-控制器(MVC)或模型-视图-视图模型(MVVM)架构:如 Vue.js 和 Angular,它们通过观察者模式实现数据双向绑定,自动更新视图。
  3. Redux 架构:React 组件订阅 Redux Store 的状态变化,状态变化时通知订阅者更新。
  4. WebSocket 实时数据推送:使用 WebSocket 实现实时通信,前端应用订阅 WebSocket 数据,一旦服务端有数据推送,前端应用就会接收到数据并进行处理。
  5. 自定义事件系统:复杂前端应用中实现跨组件通信,通过事件中心注册监听和触发自定义事件,解耦组件关系。

典型案例

  1. Vue.js:使用观察者模式实现响应式数据绑定,自动监听数据变化并更新视图。
  2. MobX:通过 observable、computed 和 reaction 实现高效的状态管理,并自动同步状态变化到视图。
  3. RxJS:提供了一组强大的 API 用于处理事件流和异步数据流,基于 Observable 和 Observer 模型,通过操作符处理数据流。
  4. Event Emitter/Event Bus:使用事件发布订阅系统实现模块间解耦通信。

观察者模式在前端开发中提供了一种有效的方式来实现组件或模块间的解耦通信。通过应用观察者模式,前端开发者可以构建出响应性强,更加灵活和易于维护的应用。众多流行前端框架和库采用观察者模式来实现响应式编程,从而提升开发效率和用户体验。