先说痛点:我在做 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)
)
一句话总结:把「变化来源」和「响应动作」彻底解耦,代码像乐高,需求再多也能拼。