前端中的观察者和发布订阅设计模式的区别和应用

前言

观察者模式(Observer Pattern)和发布订阅模式(Publish-Subscribe Pattern)都是常见的行为设计模式,用于实现对象之间的解耦和通信。它们之间的主要区别在于对象之间的关系和通信方式。

观察者模式:

  • 关系:观察者模式中存在一个被观察者(Subject)和多个观察者(Observer)之间的一对多关系。当被观察者的状态发生变化时,所有观察者都会收到通知并进行相应的更新。
  • 通信方式:被观察者直接通知观察者,观察者通过注册或订阅的方式与被观察者建立联系,当被观察者状态改变时,观察者会得到通知。

发布订阅模式:

  • 关系:发布订阅模式中引入了一个消息代理(Message Broker)或事件通道(Event Channel),发布者(Publisher)将消息发布到通道中,订阅者(Subscriber)从通道中订阅感兴趣的消息。
  • 通信方式:发布者和订阅者之间通过消息代理进行通信,发布者不需要直接知道订阅者的存在,订阅者也不需要直接知道发布者的存在,它们之间通过消息通道进行解耦。

形象例子:

假设一个新闻发布系统,有多个用户(观察者)希望订阅不同类型的新闻。这里可以使用观察者模式和发布订阅模式来实现:

  • 观察者模式:每个用户订阅自己感兴趣的新闻频道(观察者注册到被观察者),当某一类型的新闻发布时,所有订阅该类型的用户都会收到通知并查看相关新闻。
  • 发布订阅模式:新闻发布系统将不同类型的新闻发布到对应的频道(消息通道),用户可以选择订阅自己感兴趣的频道(订阅者订阅消息通道),当新闻发布到频道时,订阅该频道的用户会收到相关新闻。

通过消息代理实现发布者与订阅者的解耦在发布订阅模式中有以下好处:

  1. 松耦合:发布者和订阅者之间的解耦使得它们可以独立演化,互不影响。发布者无需关注订阅者的存在,而订阅者也无需关注具体的发布者。这种松耦合的设计使得系统更加灵活、可扩展和易于维护。
  2. 可伸缩性:发布订阅模式支持多个发布者和多个订阅者之间的通信。当系统需要增加新的发布者或订阅者时,只需要将其连接到消息代理即可,而不需要修改已有的发布者或订阅者的代码。这种可伸缩性使得系统可以方便地进行扩展。
  3. 解耦复杂性:由于发布者和订阅者之间通过消息代理进行通信,发布者无需关注订阅者的逻辑处理,而订阅者也无需关注发布者的具体实现。这样可以将复杂的通信逻辑集中在消息代理中,简化了发布者和订阅者的逻辑,提高了系统的可读性和可维护性。
  4. 灵活性:发布订阅模式可以支持不同类型的消息和多个订阅者之间的多对多关系。发布者可以根据需要选择要发布的消息类型,而订阅者可以根据自己的需求选择订阅感兴趣的消息类型。这种灵活性使得系统可以根据实际情况进行定制化的通信。

总而言之,通过消息代理实现发布者与订阅者的解耦可以提供松耦合、可伸缩性、解耦复杂性和灵活性等好处。这使得系统更加灵活、可扩展和易于维护,并支持复杂的通信需求。

具体应用

观察者模式

见之前写的 《前端分享--ES6之Promise源码系列【干货】 》 中通过分析Promise的调用流程:

  • Promise的构造方法接收一个executor(),在new Promise()时就立刻执行这个executor回调
  • executor()内部的异步任务被放入宏/微任务队列,等待执行
  • then()被执行,收集成功/失败回调,放入成功/失败队列
  • executor()的异步任务被执行,触发resolve/reject,从成功/失败队列中取出回调依次执行

意识到这是个「观察者模式」,这种收集依赖 -> 触发通知 -> 取出依赖执行 的方式,被广泛运用于观察者模式的实现,在Promise里,执行顺序是then收集依赖 -> 异步触发resolve -> resolve执行依赖

发布订阅模式

《前端接口防止重复请求实现方案》中通过发布订阅模式去解决接口重复调用的问题

typescript 复制代码
import axios from "axios"

let instance = axios.create({
    baseURL: "/api/"
})

// 发布订阅
class EventEmitter {
    constructor() {
        this.event = {}
    }
    on(type, cbres, cbrej) {
        if (!this.event[type]) {
            this.event[type] = [[cbres, cbrej]]
        } else {
            this.event[type].push([cbres, cbrej])
        }
    }

    emit(type, res, ansType) {
        if (!this.event[type]) return
        else {
            this.event[type].forEach(cbArr => {
                if(ansType === 'resolve') {
                    cbArr[0](res)
                }else{
                    cbArr[1](res)
                }
            });
        }
    }
}


// 根据请求生成对应的key
function generateReqKey(config, hash) {
    const { method, url, params, data } = config;
    return [method, url, JSON.stringify(params), JSON.stringify(data), hash].join("&");
}

// 存储已发送但未响应的请求
const pendingRequest = new Set();
// 发布订阅容器
const ev = new EventEmitter()

// 添加请求拦截器
instance.interceptors.request.use(async (config) => {
    let hash = location.hash
    // 生成请求Key
    let reqKey = generateReqKey(config, hash)
    
    if(pendingRequest.has(reqKey)) {
        // 如果是相同请求,在这里将请求挂起,通过发布订阅来为该请求返回结果
        // 这里需注意,拿到结果后,无论成功与否,都需要return Promise.reject()来中断这次请求,否则请求会正常发送至服务器
        let res = null
        try {
            // 接口成功响应
          res = await new Promise((resolve, reject) => {
                    ev.on(reqKey, resolve, reject)
                })
          return Promise.reject({
                    type: 'limiteResSuccess',
                    val: res
                })
        }catch(limitFunErr) {
            // 接口报错
            return Promise.reject({
                        type: 'limiteResError',
                        val: limitFunErr
                    })
        }
    }else{
        // 将请求的key保存在config
        config.pendKey = reqKey
        pendingRequest.add(reqKey)
    }

    return config;
  }, function (error) {
    return Promise.reject(error);
  });

// 添加响应拦截器
instance.interceptors.response.use(function (response) {
    // 将拿到的结果发布给其他相同的接口
    handleSuccessResponse_limit(response)
    return response;
  }, function (error) {
    return handleErrorResponse_limit(error)
  });

// 接口响应成功
function handleSuccessResponse_limit(response) {
      const reqKey = response.config.pendKey
    if(pendingRequest.has(reqKey)) {
      let x = null
      try {
        x = JSON.parse(JSON.stringify(response))
      }catch(e) {
        x = response
      }
      pendingRequest.delete(reqKey)
      ev.emit(reqKey, x, 'resolve')
      delete ev.reqKey
    }
}

// 接口走失败响应
function handleErrorResponse_limit(error) {
    if(error.type && error.type === 'limiteResSuccess') {
      return Promise.resolve(error.val)
    }else if(error.type && error.type === 'limiteResError') {
      return Promise.reject(error.val);
    }else{
      const reqKey = error.config.pendKey
      if(pendingRequest.has(reqKey)) {
        let x = null
        try {
          x = JSON.parse(JSON.stringify(error))
        }catch(e) {
          x = error
        }
        pendingRequest.delete(reqKey)
        ev.emit(reqKey, x, 'reject')
        delete ev.reqKey
      }
    }
      return Promise.reject(error);
}

export default instance;

总结

参考:

juejin.cn/post/734184... blog.csdn.net/weixin_4581...

相关推荐
崔庆才丨静觅5 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60615 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了6 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅6 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅6 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅6 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment7 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅7 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊7 小时前
jwt介绍
前端
爱敲代码的小鱼7 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax