前端中的观察者和发布订阅设计模式的区别和应用
2024-03-12
1461 字约 5 分钟
...前言
观察者模式(Observer Pattern)和发布订阅模式(Publish-Subscribe Pattern)都是常见的行为设计模式,用于实现对象之间的解耦和通信。它们之间的主要区别在于对象之间的关系和通信方式。
观察者模式:
- 关系:观察者模式中存在一个被观察者(Subject)和多个观察者(Observer)之间的一对多关系。当被观察者的状态发生变化时,所有观察者都会收到通知并进行相应的更新。
- 通信方式:被观察者直接通知观察者,观察者通过注册或订阅的方式与被观察者建立联系,当被观察者状态改变时,观察者会得到通知。
发布订阅模式:
- 关系:发布订阅模式中引入了一个消息代理(Message Broker)或事件通道(Event Channel),发布者(Publisher)将消息发布到通道中,订阅者(Subscriber)从通道中订阅感兴趣的消息。
- 通信方式:发布者和订阅者之间通过消息代理进行通信,发布者不需要直接知道订阅者的存在,订阅者也不需要直接知道发布者的存在,它们之间通过消息通道进行解耦。
形象例子:
假设一个新闻发布系统,有多个用户(观察者)希望订阅不同类型的新闻。这里可以使用观察者模式和发布订阅模式来实现:
- 观察者模式:每个用户订阅自己感兴趣的新闻频道(观察者注册到被观察者),当某一类型的新闻发布时,所有订阅该类型的用户都会收到通知并查看相关新闻。
- 发布订阅模式:新闻发布系统将不同类型的新闻发布到对应的频道(消息通道),用户可以选择订阅自己感兴趣的频道(订阅者订阅消息通道),当新闻发布到频道时,订阅该频道的用户会收到相关新闻。
通过消息代理实现发布者与订阅者的解耦在发布订阅模式中有以下好处:
- 松耦合:发布者和订阅者之间的解耦使得它们可以独立演化,互不影响。发布者无需关注订阅者的存在,而订阅者也无需关注具体的发布者。这种松耦合的设计使得系统更加灵活、可扩展和易于维护。
- 可伸缩性:发布订阅模式支持多个发布者和多个订阅者之间的通信。当系统需要增加新的发布者或订阅者时,只需要将其连接到消息代理即可,而不需要修改已有的发布者或订阅者的代码。这种可伸缩性使得系统可以方便地进行扩展。
- 解耦复杂性:由于发布者和订阅者之间通过消息代理进行通信,发布者无需关注订阅者的逻辑处理,而订阅者也无需关注发布者的具体实现。这样可以将复杂的通信逻辑集中在消息代理中,简化了发布者和订阅者的逻辑,提高了系统的可读性和可维护性。
- 灵活性:发布订阅模式可以支持不同类型的消息和多个订阅者之间的多对多关系。发布者可以根据需要选择要发布的消息类型,而订阅者可以根据自己的需求选择订阅感兴趣的消息类型。这种灵活性使得系统可以根据实际情况进行定制化的通信。
总而言之,通过消息代理实现发布者与订阅者的解耦可以提供松耦合、可伸缩性、解耦复杂性和灵活性等好处。这使得系统更加灵活、可扩展和易于维护,并支持复杂的通信需求。
具体应用
观察者模式
见之前写的 《前端分享--ES6之Promise源码系列【干货】 》 中通过分析Promise的调用流程:
Promise的构造方法接收一个executor(),在new Promise()时就立刻执行这个executor回调executor()内部的异步任务被放入宏/微任务队列,等待执行then()被执行,收集成功/失败回调,放入成功/失败队列executor()的异步任务被执行,触发resolve/reject,从成功/失败队列中取出回调依次执行
意识到这是个「观察者模式」,这种收集依赖 -> 触发通知 -> 取出依赖执行 的方式,被广泛运用于观察者模式的实现,在Promise里,执行顺序是then收集依赖 -> 异步触发resolve -> resolve执行依赖。
发布订阅模式
《前端接口防止重复请求实现方案》中通过发布订阅模式去解决接口重复调用的问题

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)
}
});
}
}
}
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
let reqKey = generateReqKey(config, hash)
if(pendingRequest.has(reqKey)) {
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{
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;
总结

参考:
如果您觉得这篇文章有帮助,请点个赞吧~
相关文章
更多文章 →八股文2026-08-20
Embedding 向量模型:从语义表示到相似度计算
前言 大模型「读懂」文字靠的是 token,但 token 之间只有离散的编号关系,模型并不知道「苹果」和「李子」在语义上很近。要让程序能「理解」两段文字的相似程度,必须先把文本映射成一个 高维向量 ,再用几何方法比较。这一步就是 Embedding。 本篇基于我本地 今天的真实代码,从语义表示讲到余弦相似度,并复盘几个踩过的真实坑。 一、为什么需要 Embedding 传统关键词检索是「字面匹配」: Embedding 做的是「语义匹...
面试
八股文2026-08-19
前端面试100题
前端面试 100 题 适用方向:中高级前端 / React / Vue / Next.js / Nuxt / TypeScript / 工程化 / 实时通信 / Electron / Node.js / AI 应用前端 使用方式:优先掌握“标准回答”,再练“面试官追问”,最后把“结合你的简历怎么答”组织成自己的项目故事。 说明 “结合你的简历怎么答”只使用你简历中已经出现的项目与技术事实。 “标准回答 / 追问”属于通用前端知识总结,用...
面试
八股文2026-08-19
深入理解 JavaScript 原型与原型链:从关系图到核心逻辑 - heshanwan - 博客园
在 JavaScript 世界里, 原型(Prototype)和原型链(Prototype Chain) 是理解对象继承、属性查找机制的基石。很多开发者初学时对它们 “又爱又恨”,这篇文章将结合经典关系图,用通俗易懂的方式拆解原型与原型链的核心逻辑,帮你彻底掌握这套机制! 一、先搞懂几个核心概念 在分析关系图前,先明确 JavaScript 中与原型相关的关键概念,避免后续混淆: 1\. 函数对象与普通对象 函数对象 :由...
面试
八股文2026-08-17
JavaScript 闭包
摘要 :闭包(Closure)是 函数与其词法环境(Lexical Environment)的组合 ——一个函数可以"记住"并访问它被创建时所处的外部作用域中的变量,即使它在该作用域之外被执行。一句话记忆: 函数打包了出生地的变量,走到哪都能用 。核心要点:① 内部函数引用外部变量即形成闭包;② 闭包实现变量私有化/数据封装/柯里化;③ 潜在风险是内存泄漏和循环引用;④ 避免泄漏需手动解除引用和及时移除监听器。 一、什么是闭包 1.1...
面试
八股文2026-08-17
this 指向与 call / apply / bind
第 1 段(10min)回顾与预习:为什么必须搞懂 this 1.1 先接昨日(闭包)的线 昨天你学了 闭包 。闭包解决的是"函数记住它定义时所在作用域里的变量"。 闭包回答的问题是: “这个函数能访问到哪些变量?” → 看定义位置(词法作用域)。 而今天的主角 回答的是一个完全不同的问题: “这个函数执行时,站在谁的角度说话?” → 看 调用位置 (谁调用的、怎么调用的)。 一句话概括两者的分工: 变量看 定义处 , 看 调用处 。这...
面试
八股文2026-08-17
事件循环与宏任务 / 微任务
第 1 段(10min)回顾与预习:接住昨天的线 1.1 先接昨日(原型链)的线 昨天你学了 对象怎么找属性 (沿原型链上溯)。今天换一个维度: 代码怎么排队执行 。 把前三天的知识串成一句话,你已经有了 JS 的三张地图: 1.2 先凭直觉回答 4 个小问题(不用对,先写下答案) 学完第 2、3、4 段再回来对照。第 1 段只做一件事:记住画面—— JS 单线程,但靠"排队 + 轮转"假装多线程 。 第 2 段(15min)知识块①:...
面试
评论
请登录后发表评论
去登录