通过观察者模式,我们可以将某些对象(观察者)订阅到另一个称为可观察对象的对象。每当事件发生时,可观察者都会通知其所有观察者。
一个可观察对象通常包含 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>
);
}
我们希望跟踪用户与应用程序的交互。每当用户单击按钮或切换开关时,我们都希望使用时间戳记录此事件。除了记录之外,我们还想创建一个 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
函数不知道 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>
);
}
每当事件发生时, logger
和 toastify
函数都会收到通知。现在我们只需要实现实际通知可观察对象的函数: handleClick
和 handleToggle
函数。这些函数应该调用可观察对象的 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>
);
}
我们刚刚完成了整个流程: handleClick
和 handleToggle
用数据调用观察者上的 notify
方法,之后观察者通知订阅者: logger
和 toastify
在这种情况下起作用。
每当用户与任一组件交互时, logger
和 toastify
函数都会收到我们传递给 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 具有大量与可观察模式配合使用的内置功能和示例。
优点
使用观察者模式是强制关注点分离和单一职责原则的好方法。观察者对象与可观察对象并不紧密耦合,并且可以随时解耦。可观察对象负责监视事件,而观察者只处理接收到的数据。
缺点
如果观察者变得过于复杂,则在通知所有订阅者时可能会导致性能问题。