观察者模式
使用 观察者模式 ,我们可以将某些对象( 观察者 )订阅到另外一些称之为 可观察对象 。每当事件发生时,可观察对象都会通知它的所有观察者!
一个可观察对象通常包含4个重要部分(译者注:原文这里写的是3个部分):
observers
:一个观察者的数组,每当特定事件发生时都会收到通知subscribe()
:一个将观察者添加到观察者列表的方法unsubscribe()
:一个从观察者列表中删除观察者的方法notify()
:一个在特定事件发生时通知所有观察者的方法
很好,让我们来创建一个可观察对象!其中一种简单的创建方式是使用 ES6 class 。
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>
);
}
我们想要持续跟踪用户与应用产生的 交互 。每当用户点击按钮或切换开关时,我们都希望使用时间戳记录此事件。除了记录,我们还想创建一个弹窗通知,在事件发生时显示!
本质上,我们想要做的是:
每当用户调用 handleClick
或 handleToggle
函数时,这些函数都会调用观察者的 notify
方法。notify
方法将 handleClick
或 handleToggle
函数传递的数据通知给所有订阅者!
首先,让我们创建 logger
和 toastify
函数。这些函数最终会从 notify
方法接收数据。
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
方法的数据!
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 将观察者模式与迭代器模式以及使用集合的函数式编程相结合,以满足对管理事件序列需求的理想方式。—— 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 有大量内置功能和示例,可以与可观察模式一起使用。
优点
使用观察者模式是实现 关注点分离 和单一职责原则的好方法。观察者对象与可观察对象没有紧密耦合,可以随时(解除)耦合。可观察对象负责监听事件,而观察者只是处理接收到的数据。
缺点
如果观察者变得过于复杂,在通知所有订阅者时可能会导致性能问题。
参考
原文链接:Observer Pattern