通过观察者模式,我们可以订阅某些对象(即观察者)到另一个对象(称为被观察者)。每当事件发生时,被观察者会通知其所有观察者!
可观察对象通常包含 4 个重要部分:
observers
:每当发生特定事件时都会收到通知的观察者列表subscribe()
:一种将观察者添加到观察者列表的方法unsubscribe()
:一种从观察者列表中删除观察者的方法notify()
:一种在特定事件发生时通知所有观察者的方法
让我们来创建一个 Observable!创建 Observable 的一种简单方法是通过使用 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>
);
}
我们希望跟踪用户与应用程序的交互。每当用户单击按钮或切换开关时,我们都希望使用时间戳记录此事件。除了记录它之外,我们还想创建一个 Toast 通知,每当事件发生时都会显示!
从本质上讲,我们想要做的是以下几点:
每当用户调用 handleClick
或 handleToggle
函数时,这些函数都会在观察者上调用notify
方法。notify
方法使用 handleClick
或 handleToggle
函数传递的数据通知所有订阅者!
首先,让我们创建 logger
和 toastify
函数。这些函数最终将从 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>
);
}
目前,logger
和 toastify
函数无法识别可观察对象:可观察对象还不能通知它们!为了让它们成为观察者,我们必须对可观察对象使用 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>
);
}
每当事件发生时,logger
和 toastify
函数都会收到通知。现在我们只需要实现实际通知可观察对象的函数:handleClick
和 handleToggle
函数!这些函数应调用可观察对象的 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>
);
}
棒!我们刚刚完成了整个流程:handleClick
和 handleToggle
使用数据在观察者上调用notify
方法,然后观察者通知订阅者:在本例中为 logger
和 toastify
函数。
每当用户与任一组件交互时,logger
和 toastify
函数都会收到我们传递给 notify
方法的数据的通知!
// App.js
import React from "react";
import { Button, Switch, FormControlLabel } from "@material-ui/core";
import { ToastContainer, toast } from "react-toastify";
import observable from "./Observable";
function handleClick() {
observable.notify("User clicked button!");
}
function handleToggle() {
observable.notify("User toggled switch!");
}
function logger(data) {
console.log(`${Date.now()} ${data}`);
}
function toastify(data) {
toast(data, {
position: toast.POSITION.BOTTOM_RIGHT,
closeButton: false,
autoClose: 2000
});
}
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>
);
}
尽管我们可以以多种方式使用观察者模式,但在处理基于事件的异步数据时,它可能非常有用。也许您希望某些组件在某些数据下载完成时收到通知,或者每当用户向留言板发送新消息时,所有其他成员都应收到通知。
个案研究
使用可观察模式的流行库是 RxJS。
ReactiveX 将 Observer 模式与 Iterator 模式以及函数式编程与集合相结合,以满足对管理事件序列的理想方式的需求。- RxJS
使用 RxJS,我们可以创建可观察对象并订阅某些事件!让我们看一下他们的文档中介绍的一个示例,该示例记录了用户是否在文档中拖动。
// index.js
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 具有大量与可观察模式一起使用的内置功能和示例。
优点
使用观察者模式是强制执行关注点分离和单一责任原则的好方法。观察者对象与可观察对象没有紧密耦合,可以随时(解耦)。可观察对象负责监视事件,而观察者只是处理接收到的数据。
缺点
如果观察者变得过于复杂,则在通知所有订阅者时可能会导致性能问题。