前端中的观察者和发布订阅设计模式的区别和应用
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-27
定时器按顺序播放多个音频,切后台音频会乱
本身不会补触发,但 它会被系统级冻结 ——iOS Safari 后台完全停止计时,切回前台后 只补执行一次 (不是不补,而是"缺失的中间状态补不上")。间隔短的不会出大问题, 间隔长的会出现"音频流被压缩" ——切回来后本该播 5 分钟的间隙,实际只过了 3 分钟,结果整段对不上。 核心原则 :定时器只能用来"提醒一次", 真正决定"该不该播"的是绝对时间戳 。 setTimeout 在后台会发生什么(按平台) | 平台 | 后台行为...
面试
八股文2026-08-27
线上项目白屏的原因
白屏的本质是 渲染管线某一环断了 ——可能是资源、JS、接口、路由、样式、兼容性任一环节出问题。排查按"控制台 → 网络 → DOM → 环境"四步定位。 本质(前端类比) 把网页想成一栋楼: 白屏 ≠ 一定是同一种原因——这是面试想听的层次。 六大类原因(按出现频率) | 类别 | 典型表现 | 真实案例 | | : | : | : | | ① 资源加载失败 | DOM 是空的 | 入口 JS 404、CDN 挂了、CSS 阻塞 |...
面试
八股文2026-08-27
背景图就是 1MB 大图,怎么优化
1MB 大背景图优化分 三步 :压缩体积(10 30x)、按需加载(按设备/视口/网速)、渲染期优化(GPU 合成)。背景图跟 不同——它是 CSS,不走浏览器的原生懒加载机制,得手动处理。 背景图 vs 的关键区别(先讲清楚这个) 这是面试官想听的"针对性认知"——背景图不是普通图片,不能套通用方案。 三步优化(按优先级) 第 1 步:压缩体积(最重要,立竿见影) 1MB 的来源一般是这几种 ,对应解决方案: | 原始问题 | 体积来...
面试
八股文2026-08-27
项目里很多图片和视频,怎么优化
图片视频优化分 四层 :网络层(CDN/格式)、加载层(懒加载/预加载)、渲染层(解码/缓存)、业务层(按需/降级)。面试要把这四层都讲到位才算有体系。 四层优化模型 第 1 层:网络层(省钱、省时间) 核心目标:让资源体积小、让用户拿到资源快 | 手段 | 作用 | 关键点 | | : | : | : | | 图片格式 | WebP/AVIF 比 JPEG 小 25 50% | 兼容 fallback | | 视频格式 | H.265...
面试
八股文2026-08-20
Embedding 向量模型:从语义表示到相似度计算
前言 大模型「读懂」文字靠的是 token,但 token 之间只有离散的编号关系,模型并不知道「苹果」和「李子」在语义上很近。要让程序能「理解」两段文字的相似程度,必须先把文本映射成一个 高维向量 ,再用几何方法比较。这一步就是 Embedding。 本篇基于我本地 今天的真实代码,从语义表示讲到余弦相似度,并复盘几个踩过的真实坑。 一、为什么需要 Embedding 传统关键词检索是「字面匹配」: Embedding 做的是「语义匹...
面试
八股文2026-08-19
前端面试100题
前端面试 100 题 适用方向:中高级前端 / React / Vue / Next.js / Nuxt / TypeScript / 工程化 / 实时通信 / Electron / Node.js / AI 应用前端 使用方式:优先掌握“标准回答”,再练“面试官追问”,最后把“结合你的简历怎么答”组织成自己的项目故事。 说明 “结合你的简历怎么答”只使用你简历中已经出现的项目与技术事实。 “标准回答 / 追问”属于通用前端知识总结,用...
面试
评论
请登录后发表评论
去登录