observer pattern 最简上手笔记
先说痛点:我在做 React 项目时,最怕「一个组件里的交互影响到五六个毫不相干的兄弟组件」。早期我把 setState
一层层传下去,结果 props 里飘满了 callback,组件拆一半就得全重写。后来我用了 observer 模式,代码瞬间清爽。
我先踩过的坑
- 把逻辑全写在父组件,state 和 UI 绑死,改需求就得全部重写
- 用 Context Provider 绕大圈,render 频繁触发,写起来像套娃
- 忘掉
off/unsubscribe
,切一次路由就内存泄露,Chrome Memory 里一片红
现在我直接三步解决
- 先写一个轻量 observable(就 30 行)
// Observable.ts
export default class Observable<T = any> {private observers: Array<(data: T) => void> = []subscribe(fn: (data: T) => void) {this.observers.push(fn)// 顺手返回一个解绑函数,免得我忘了return () => {this.observers = this.observers.filter((o) => o !== fn)}}unsubscribe(fn: (data: T) => void) {this.observers = this.observers.filter((o) => o !== fn)}notify(data: T) {this.observers.forEach((observer) => observer(data))}
}
- 组件里只管「触发」和「监听」
// App.tsx
import observable from './Observable'
import { toast } from 'react-toastify'const logger = (msg: string) => console.log(Date.now(), msg)
const toastify = (msg: string) => toast(msg, { position: 'bottom-right', autoClose: 2000 })export default function App() {React.useEffect(() => {// 组件加载时一次性注册const offLog = observable.subscribe(logger)const offToast = observable.subscribe(toastify)return () => {// 卸载时解绑,不留后患offLog()offToast()}}, [])const handleClick = () => observable.notify('按钮被点')const handleToggle = () => observable.notify('开关切了')return (<><button onClick={handleClick}>点我</button><input type="checkbox" onChange={handleToggle} /></>)
}
要点:把 subscribe
丢进 useEffect
,返回的清理函数里解绑,永远不会内存泄露。
- 需求再多,也只需「加订阅」——组件代码零改动
想再发埋点?
const track = (msg) => fetch('/analytics', { method: 'POST', body: msg })
observable.subscribe(track) // 一行搞定,不改任何旧组件
扩展阅读
- RxJS:把上面的小玩具换成 RxJS,可处理异步流、节流、防抖、合并事件等高阶需求
典型代码:
import { fromEvent, merge } from 'rxjs'
import { mapTo, sample } from 'rxjs/operators'merge(fromEvent(document, 'mousedown').pipe(mapTo(false)),fromEvent(document, 'mousemove').pipe(mapTo(true))
).pipe(sample(fromEvent(document, 'mouseup'))).subscribe((isDragging) =>console.log('刚才拖动了吗?', isDragging))
一句话总结:把「变化来源」和「响应动作」彻底解耦,代码像乐高,需求再多也能拼。