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

前言

观察者模式(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...

相关推荐
Fan_web15 分钟前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常17 分钟前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式
莹雨潇潇1 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
严文文-Chris1 小时前
【设计模式-中介者模式】
设计模式·中介者模式
Jiaberrr1 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
刷帅耍帅1 小时前
设计模式-中介者模式
设计模式·中介者模式
刷帅耍帅2 小时前
设计模式-组合模式
设计模式·组合模式
Tiffany_Ho2 小时前
【TypeScript】知识点梳理(三)
前端·typescript
安冬的码畜日常3 小时前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js
刷帅耍帅3 小时前
设计模式-命令模式
设计模式·命令模式