通过观察者模式,我们可以将某些对象(观察者)订阅到另一个称为可观察对象的对象。每当事件发生时,可观察者都会通知其所有观察者。

一个可观察对象通常包含 3 个重要部分:

observers :观察者数组,每当特定事件发生时都会收到通知

subscribe() :将观察者添加到观察者列表的方法

unsubscribe() :从观察者列表中删除观察者的方法

notify() :每当特定事件发生时通知所有观察者的方法

创建一个简单的方法是使用 ES6 类。

class Observable {
  constructor() {
    this.observers = [];
  }

  subscribe(func) {
    this.observers.push(func);
  }

  unsubscribe(func) {
    this.observers = this.observers.filter((observer) => observer !== func);
  }

  notify(data) {
    this.observers.forEach((observer) => observer(data));
  }
}

我们现在可以使用 subscribe 方法将观察者添加到观察者列表中,使用 unsubscribe 方法删除观察者,并使用 notify 方法通知所有订阅者。

现在我们有一个非常基本的应用程序,仅包含两个组件: ButtonSwitch

export default function App() {
  return (
    <div className="App">
      <Button>Click me!</Button>
      <FormControlLabel control={<Switch />} />
    </div>
  );
}

我们希望跟踪用户与应用程序的交互。每当用户单击按钮或切换开关时,我们都希望使用时间戳记录此事件。除了记录之外,我们还想创建一个 Toast 通知,每当事件发生时就会显示。

每当用户调用 handleClickhandleToggle 函数时,这些函数都会调用观察者上的 notify 方法。 notify 方法用 handleClickhandleToggle 函数传递的数据通知所有订阅者。

首先,让我们创建 loggertoastify 函数。这些函数最终将从 notify 方法接收 data

import { ToastContainer, toast } from "react-toastify";

function logger(data) {
  console.log(`${Date.now()} ${data}`);
}

function toastify(data) {
  toast(data);
}

export default function App() {
  return (
    <div className="App">
      <Button>Click me!</Button>
      <FormControlLabel control={<Switch />} />
      <ToastContainer />
    </div>
  );
}

目前, loggertoastify 函数不知道 observable,observable 还无法通知它们。为了使它们成为观察者,我们必须使用可观察对象上的 subscribe 方法来订阅它们。

import { ToastContainer, toast } from "react-toastify";

function logger(data) {
  console.log(`${Date.now()} ${data}`);
}

function toastify(data) {
  toast(data);
}

observable.subscribe(logger);
observable.subscribe(toastify);

export default function App() {
  return (
    <div className="App">
      <Button>Click me!</Button>
      <FormControlLabel control={<Switch />} />
      <ToastContainer />
    </div>
  );
}

每当事件发生时, loggertoastify 函数都会收到通知。现在我们只需要实现实际通知可观察对象的函数: handleClickhandleToggle 函数。这些函数应该调用可观察对象的 notify 方法,并传递观察者应该接收的数据。

import { ToastContainer, toast } from "react-toastify";

function logger(data) {
  console.log(`${Date.now()} ${data}`);
}

function toastify(data) {
  toast(data, {
    position: toast.POSITION.BOTTOM_RIGHT,
    closeButton: false,
    autoClose: 2000,
  });
}

function handleClick() {
  observable.notify("User clicked button!");
}

function handleToggle() {
  observable.notify("User toggled switch!");
}

observable.subscribe(logger);
observable.subscribe(toastify);

export default function App() {
  return (
    <div className="App">
      <Button variant="contained" onClick={handleClick}>
        Click me!
      </Button>
      <FormControlLabel
        control={<Switch name="" onChange={handleToggle} />}
        label="Toggle me!"
      />
      <ToastContainer />
    </div>
  );
}

我们刚刚完成了整个流程: handleClickhandleToggle 用数据调用观察者上的 notify 方法,之后观察者通知订阅者: loggertoastify 在这种情况下起作用。

每当用户与任一组件交互时, loggertoastify 函数都会收到我们传递给 notify 方法的数据的通知。

我们可以通过多种方式使用观察者模式,它在处理异步、基于事件的数据时非常有用。当你希望每在某些数据下载完成时,或者每当用户向留言板发送新消息时,某些组件都会收到通知,并且所有其他成员都应该收到通知。

案例分析

使用可观察模式的流行库有 RxJS。

ReactiveX combines the Observer pattern with the Iterator pattern and functional programming with collections to fill the need for an ideal way of managing sequences of events. - RxJS

使用 RxJS,我们可以创建可观察对象并订阅某些事件。让我们看一下他们的文档中包含的一个示例,该示例记录用户是否在文档中拖动。

import React from "react";
import ReactDOM from "react-dom";
import { fromEvent, merge } from "rxjs";
import { sample, mapTo } from "rxjs/operators";

import "./styles.css";

merge(
  fromEvent(document, "mousedown").pipe(mapTo(false)),
  fromEvent(document, "mousemove").pipe(mapTo(true))
)
  .pipe(sample(fromEvent(document, "mouseup")))
  .subscribe((isDragging) => {
    console.log("Were you dragging?", isDragging);
  });

ReactDOM.render(
  <div className="App">Click or drag anywhere and check the console!</div>,
  document.getElementById("root")
);

RxJS 具有大量与可观察模式配合使用的内置功能和示例。

优点

使用观察者模式是强制关注点分离和单一职责原则的好方法。观察者对象与可观察对象并不紧密耦合,并且可以随时解耦。可观察对象负责监视事件,而观察者只处理接收到的数据。

缺点

如果观察者变得过于复杂,则在通知所有订阅者时可能会导致性能问题。