对我们的下一本书感兴趣吗?了解更多关于 使用 React 构建大型 JavaScript Web 应用程序

设计模式

观察者模式

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


一个被观察者对象通常包含 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 方法通知所有订阅者。

让我们用这个被观察者来构建一些东西。我们有一个非常基本的应用程序,它只包含两个组件:一个Button和一个Switch

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

我们希望跟踪用户与应用程序的**交互**。每当用户点击按钮或切换开关时,我们希望用时间戳记录此事件。除了记录它,我们还想创建一个每当事件发生时就会出现的提示通知!

本质上,我们要做的是以下几点

每当用户调用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函数并不知道被观察者:被观察者还不能通知它们!为了使它们成为观察者,我们必须使用被观察者的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);
}

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

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

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

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

太棒了!我们刚刚完成了整个流程:handleClickhandleToggle使用数据调用被观察者的notify方法,之后被观察者通知订阅者:在本例中是loggertoastify函数。

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

App.js
Observable.js
1import React from "react";
2import { Button, Switch, FormControlLabel } from "@material-ui/core";
3import { ToastContainer, toast } from "react-toastify";
4import observable from "./Observable";
5
6function handleClick() {
7 observable.notify("User clicked button!");
8}
9
10function handleToggle() {
11 observable.notify("User toggled switch!");
12}
13
14function logger(data) {
15 console.log(`${Date.now()} ${data}`);
16}
17
18function toastify(data) {
19 toast(data, {
20 position: toast.POSITION.BOTTOM_RIGHT,
21 closeButton: false,
22 autoClose: 2000
23 });
24}
25
26observable.subscribe(logger);
27observable.subscribe(toastify);
28
29export default function App() {
30 return (
31 <div className="App">
32 <Button variant="contained" onClick={handleClick}>
33 Click me!
34 </Button>
35 <FormControlLabel
36 control={<Switch name="" onChange={handleToggle} />}
37 label="Toggle me!"
38 />
39 <ToastContainer />
40 </div>
41 );
42}

虽然我们可以用很多方法使用观察者模式,但它在处理**异步、基于事件的数据**时非常有用。也许你想让某些组件在某些数据下载完毕或用户向留言板发送新消息时收到通知,而所有其他成员都应该收到通知。


案例研究

一个使用观察者模式的流行库是 RxJS。

ReactiveX 将观察者模式与迭代器模式和集合的函数式编程结合起来,以满足对管理事件序列的理想方式的需求。- RxJS

使用 RxJS,我们可以创建被观察者并订阅某些事件!让我们看一下他们的文档中介绍的一个例子,该例子记录了用户是否在文档中拖动。

index.js
1import React from "react";
2import ReactDOM from "react-dom";
3import { fromEvent, merge } from "rxjs";
4import { sample, mapTo } from "rxjs/operators";
5
6import "./styles.css";
7
8merge(
9 fromEvent(document, "mousedown").pipe(mapTo(false)),
10 fromEvent(document, "mousemove").pipe(mapTo(true))
11)
12 .pipe(sample(fromEvent(document, "mouseup")))
13 .subscribe(isDragging => {
14 console.log("Were you dragging?", isDragging);
15 });
16
17ReactDOM.render(
18 <div className="App">Click or drag anywhere and check the console!</div>,
19 document.getElementById("root")
20);

RxJS 拥有大量与观察者模式一起使用的内置功能和示例。


优点

使用观察者模式是强制执行关注点分离和单一责任原则的绝佳方式。观察者对象与被观察者对象没有紧密耦合,并且可以在任何时候(解除)耦合。被观察者对象负责监控事件,而观察者只处理接收到的数据。


缺点

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


参考资料