observer pattern 最简上手笔记

发布于:2025-08-31 ⋅ 阅读:(26) ⋅ 点赞:(0)

先说痛点:我在做 React 项目时,最怕「一个组件里的交互影响到五六个毫不相干的兄弟组件」。早期我把 setState 一层层传下去,结果 props 里飘满了 callback,组件拆一半就得全重写。后来我用了 observer 模式,代码瞬间清爽。

我先踩过的坑

  • 把逻辑全写在父组件,state 和 UI 绑死,改需求就得全部重写
  • 用 Context Provider 绕大圈,render 频繁触发,写起来像套娃
  • 忘掉 off/unsubscribe,切一次路由就内存泄露,Chrome Memory 里一片红

现在我直接三步解决

  1. 先写一个轻量 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))
  }
}
  1. 组件里只管「触发」和「监听」
// 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,返回的清理函数里解绑,永远不会内存泄露。

  1. 需求再多,也只需「加订阅」——组件代码零改动
    想再发埋点?
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)
  )

一句话总结:把「变化来源」和「响应动作」彻底解耦,代码像乐高,需求再多也能拼。


网站公告

今日签到

点亮在社区的每一天
去签到