行为型(TS)

5 阅读7分钟

观察者模式

允许你定义一种一对多的订阅关系

当一个对象(被观察者 / Subject)的状态发生变化时,
所有依赖于它的对象(观察者 / Observer)都会收到通知并自动更新。

维护观察者列表

被观察者内部维护一个 Observer[]

  • 支持:

  • 添加观察者

  • 移除观察者

定义订阅机制
观察者通过 subscribe / attach 注册

  • 被观察者不关心观察者的具体实现,只关心“它们实现了统一接口”

  • 观察者 只响应通知,不相互通信

事件触发时统一通知

  • 状态变化 / 新事件发生
  • 被观察者遍历观察者列表
  • 逐个调用观察者的 update 方法

例如:
1.vue的响应式系统

2.DOM 事件系统和 MutationObserver 都是观察者模式的典型实现:

DOM 元素或被监听对象作为 Subject,内部维护监听器列表;
当事件或状态变化发生时,统一通知所有观察者。

/**
 * 观察者模式
 *
 * 特点:
 * - 一对多订阅关系
 * - 被观察者内部维护观察者列表
 * - 支持订阅 / 取消订阅
 * - 状态变化时统一通知
 * - 观察者只需遵循统一的 update 约定
 */

/**
 * 创建一个被观察者(Subject)
 */
function createSubject() {
    // 维护观察者列表          observers 是一个数组 数组里每一项 必须是一个函数(接收 state 参数、没有返回值的函数)
    const observers: Array<(state: any) => void> = []
  
    // 被观察者的内部状态
    let state: any = null
  
    /**
     * 订阅(添加观察者)
     */
    function subscribe(observer: (state: any) => void) {
      observers.push(observer)
    }
  
    /**
     * 取消订阅(移除观察者)
     */
    function unsubscribe(observer: (state: any) => void) {
      const index = observers.indexOf(observer)
      if (index !== -1) {
        observers.splice(index, 1)
      }
    }
  
    /**
     * 状态更新
     * 状态变化是“事件源”
     */
    function setState(newState: any) {
      state = newState
      notify()
    }
  
    /**
     * 统一通知所有观察者
     */
    function notify() {
      observers.forEach(observer => {
        observer(state)
      })
    }
  
    // 只暴露必要能力(封装内部实现)
    return {
      subscribe,
      unsubscribe,
      setState
    }
  }
  
  /**
   * ===============================
   * 使用示例
   * ===============================
   */
  
  // 创建被观察者
  const subject = createSubject()
  
  // 创建观察者(函数)
  const observerA = (state: any) => {
    console.log('观察者 A 收到更新:', state)
  }
  
  const observerB = (state: any) => {
    console.log('观察者 B 收到更新:', state)
  }
  
  // 订阅
  subject.subscribe(observerA)
  subject.subscribe(observerB)
  
  // 状态变化 → 通知所有观察者
  subject.setState({ count: 1 })
  
  // 取消订阅 A
  subject.unsubscribe(observerA)
  
  // 再次状态变化
  subject.setState({ count: 2 })
  
  /**
   * 输出:
   * 观察者 A 收到更新: { count: 1 }
   * 观察者 B 收到更新: { count: 1 }
   * 观察者 B 收到更新: { count: 2 }
   */
  

发布订阅模式

观察者模式中,观察者需要直接订阅被观察者,被观察者内部维护观察者列表,并在状态变化时直接通知观察者;
而发布–订阅模式通过引入中间消息代理,使发布者与订阅者不直接通信,双方只依赖消息中心,从而实现更彻底的解耦。

观察者模式

  • DOM addEventListener
  • MutationObserver
  • Vue 响应式(dep ↔ watcher)

发布–订阅模式

  • EventBus
  • mitt
  • Node.js EventEmitter
  • 消息队列(Kafka / RabbitMQ)
/**
 * 发布–订阅模式(Publish–Subscribe)
 *
 * 核心特点:
 * - 引入“消息中心 / 事件总线(Event Bus)”作为中间代理
 * - 发布者和订阅者完全解耦
 * - 发布者不关心谁订阅
 * - 订阅者不关心谁发布
 * - 通过“事件名 / topic”进行通信
 */

/**
 * 创建一个事件总线(消息中介)
 */
function createEventBus() {
    /**
     * events 是一个 Map:
     * key   -> 事件名(string)
     * value -> 订阅该事件的回调函数数组(数组里每一项 必须是一个函数(接收 data 参数、没有返回值的函数))
     */
    const events: Map<string, Array<(data: any) => void>> = new Map()
  
    /**
     * 订阅事件
     * @param eventName 事件名 / topic
     * @param handler   回调函数(订阅者)
     */
    function on(eventName: string, handler: (data: any) => void) {
      // 如果该事件还没有订阅者,先初始化一个数组
      if (!events.has(eventName)) {
        events.set(eventName, [])
      }
  
      // 将订阅者加入事件对应的列表 
      // !非空断言 前面已经判断是否存在该订阅者 不存在则初始化一个数组 
      events.get(eventName)!.push(handler)
    }
  
    /**
     * 取消订阅
     */
    function off(eventName: string, handler: (data: any) => void) {
      const handlers = events.get(eventName)
      if (!handlers) return
  
      const index = handlers.indexOf(handler)
      if (index !== -1) {
        handlers.splice(index, 1)
      }
    }
  
    /**
     * 发布事件
     * @param eventName 事件名
     * @param data      事件数据
     */
    function emit(eventName: string, data: any) {
      const handlers = events.get(eventName)
      if (!handlers) return
  
      // 通知所有订阅该事件的订阅者
      handlers.forEach(handler => {
        handler(data)
      })
    }
  
    return {
      on,
      off,
      emit
    }
  }
  
  /**
   * 使用示例
   */
  
  // 创建一个全局事件总线(中介)
  const eventBus = createEventBus()
  
  /**
   * 订阅者 A
   * 只关心 "order:create" 这个事件
   */
  const subscriberA = (data: any) => {
    console.log('订阅者 A 收到订单创建事件:', data)
  }
  
  /**
   * 订阅者 B
   * 也订阅同一个事件
   */
  const subscriberB = (data: any) => {
    console.log('订阅者 B 收到订单创建事件:', data)
  }
  
  // 订阅事件
  eventBus.on('order:create', subscriberA)
  eventBus.on('order:create', subscriberB)
  
  /**
   * 发布者
   * 发布事件时:
   * - 不知道谁在监听
   * - 只负责发消息
   */
  eventBus.emit('order:create', {
    orderId: 1001,
    price: 299
  })
  
  // 取消订阅 A
  eventBus.off('order:create', subscriberA)
  
  // 再次发布事件
  eventBus.emit('order:create', {
    orderId: 1002,
    price: 499
  })
  
  /**
   * 输出:
   * 订阅者 A 收到订单创建事件: { orderId: 1001, price: 299 }
   * 订阅者 B 收到订单创建事件: { orderId: 1001, price: 299 }
   * 订阅者 B 收到订单创建事件: { orderId: 1002, price: 499 }
   */
  

模版模式

在抽象父类中定义一个算法的整体框架(执行步骤及顺序),
将其中部分可变的步骤延迟到子类实现。
子类在不改变算法结构的前提下,重写特定步骤,从而实现不同行为。

抽象父类(Template / Abstract Class)

  • 定义算法的整体流程(模板方法)
  • 实现通用逻辑
  • 声明需要子类实现的抽象步骤(或 Hook)

具体子类(Concrete Class)

  • 继承父类,复用算法结构
  • 针对流程中的某些步骤进行重写
/**
 *  父类中的模板方法
 * - update 是“模板方法”  render / shouldUpdate → 抽象步骤  commit / didUpdate → 可选步骤
 * - 流程固定
 * - 各步骤通过函数参数传入(策略 / hook)
 */

function createUpdater(options: {
    shouldUpdate?: (prevState: any, nextState: any) => boolean
    render: (state: any) => any
    commit?: (vdom: any) => void
    didUpdate?: () => void
}) {
    // 内部状态(闭包)
    let state: any = null

    /**
     * ===== 模板方法(流程不可变)=====
     */
    function update(nextState: any) {
        const prevState = state

        // 1. 是否更新(可选 hook)
        if (
            options.shouldUpdate &&
            !options.shouldUpdate(prevState, nextState)
        ) {
            return
        }

        // 2. 更新状态
        state = nextState

        // 3. render(必须)
        const vdom = options.render(state)

        // 4. commit(可选 hook) ?. 如果前面的值存在(不是 null / undefined),才继续访问或执行
        options.commit?.(vdom)

        // 5. 更新完成(可选 hook)
        options.didUpdate?.()
    }

    return {
        update
    }
}



/**
 * 子类中的具体实现
 */

const counter = createUpdater({
    shouldUpdate(prev, next) {
        console.log('shouldUpdate')
        return next.count <= 5
    },

    render(state) {
        console.log('render')
        return `<div>count: ${state.count}</div>`
    },

    commit(vdom) {
        console.log('commit DOM:', vdom)
    },

    didUpdate() {
        console.log('didUpdate')
    }
})

// 触发更新
counter.update({ count: 1 })
counter.update({ count: 2 })
counter.update({ count: 6 }) // 不会更新

// 输出结果
// shouldUpdate
// render
// commit DOM: <div>count: 1</div>
// didUpdate

// shouldUpdate
// render
// commit DOM: <div>count: 2</div>
// didUpdate

// shouldUpdate false -> 不更新   模板方法提前终止,后续步骤(状态更新、render、commit、didUpdate)均不会执行。

策略模式

定义一系列算法,将每一个算法封装起来,使它们可以相互替换。
策略模式使算法的变化独立于使用算法的客户端。

模板方法:固定流程
策略模式:可替换算法

/**
 * 场景:i18n 根据语言切换文案
 *
 * 思想:
 * - 不同语言 = 不同算法(策略)
 * - 运行时根据 locale 选择策略
 * - 使用方只依赖统一接口,不关心具体实现
 */

/**
 * 这里用函数类型来表示“策略”
 */
type I18nStrategy = (key: string) => string
type Locale = 'zh' | 'en'

/**
 * 中文翻译策略
 */
const zhStrategy: I18nStrategy = (key) => {
  // 中文词典
  const dict: Record<string, string> = {
    hello: '你好',
    world: '世界'
  }

  // 如果找不到对应 key,则兜底返回 key 本身
  return dict[key] ?? key
}

/**
 * 英文翻译策略
 */
const enStrategy: I18nStrategy = (key) => {
  // 英文词典
  const dict: Record<string, string> = {
    hello: 'Hello',
    world: 'World'
  }

  return dict[key] ?? key
}

/**
 * 策略注册表
 *
 * key:策略标识(locale)
 * value:具体策略实现
 */
const strategyMap: Record<string, I18nStrategy> = {
  zh: zhStrategy,
  en: enStrategy
}

/**
 * 上下文
 *
 * Context 持有一个策略
 * 但不关心策略的具体实现
 * 只通过统一接口(I18nStrategy)调用
 */
function createI18n(locale: Locale) {
  const strategy = strategyMap[locale]

  if (!strategy) {
    throw new Error(`Unsupported locale: ${locale}`)
  }

  /**
   * 对外暴露的统一 API
   */
  function t(key: string): string {
    return strategy(key)
  }

  return { t }
}

/**
 *  使用示例(Client)
 */

// 使用中文策略
const i18nZh = createI18n('zh')
console.log(i18nZh.t('hello')) // 你好
console.log(i18nZh.t('world')) // 世界

// 使用英文策略
const i18nEn = createI18n('en')
console.log(i18nEn.t('hello')) // Hello
console.log(i18nEn.t('world')) // World

迭代器模式

不暴露集合内部表示的前提下,
提供一种统一的方式顺序访问集合中的元素

JS 中,Map 天然支持用 for...of 进行迭代

/**
 * 迭代器模式(Iterator Pattern)
 *
 * 目标:
 * - 在不暴露集合内部结构的前提下
 * - 提供一种统一的方式顺序访问集合元素
 *
 */

/**
 * 迭代器接口(Iterator Protocol)
 */
interface MyIterator<T> {
    next(): {
      value: T | undefined
      done: boolean
    }
  }
  
  /**
   * 创建一个“可迭代的集合”
   *
   * - 内部使用数组存储数据(对外不可见)
   * - 通过闭包封装内部状态
   * - 通过 [Symbol.iterator] 暴露遍历能力
   */
  function createNumberCollection() {
    // 私有数据(外部无法直接访问)
    const items: number[] = []
  
    /**
     * 添加元素
     */
    function add(value: number) {
      items.push(value)
    }
  
    /**
     * 迭代器工厂函数
     *
     * 每次调用都会创建一个新的迭代器实例
     * 用于保存独立的遍历状态
     */
    function createIterator(): MyIterator<number> {
      let index = 0
  
      return {
        next() {
          if (index < items.length) {
            return {
              value: items[index++],
              done: false
            }
          }
  
          return {
            value: undefined,
            done: true
          }
        }
      }
    }
  
    /**
     * 对外暴露的集合 API
     */
    return {
      add,
  
      /**
       * Iterable Protocol
       *
       * - 定义 [Symbol.iterator] 方法  
       *   - [Symbol.iterator] 是一个 key, 它对应的 value 必须是一个函数, 这个函数在遍历开始时被 JS 引擎调用, 并返回一个带 next 方法的迭代器对象。
       *
       * 一旦实现该方法,该对象即可被:
       * - for...of
       * - Array.from
       * - 展开运算符 (...)
       * 等语言结构遍历
       */
      [Symbol.iterator]: createIterator
    }
  }
  
  /**
   * ================================
   * 使用示例
   * ================================
   */
  
  const collection = createNumberCollection()
  
  collection.add(1)
  collection.add(2)
  collection.add(3)
  
  // for...of 会自动:
  // 1. 调用 collection[Symbol.iterator]()
  // 2. 不断执行 next()
  // 3. 直到 done === true
  for (const num of collection) {
    console.log(num)
  }
  
  // 输出:
  // 1
  // 2
  // 3