面试官:能用JS写一个发布订阅模式吗?
什么是发布订阅模式?能手写实现一下吗?它和观察者模式有区别吗?...
1 场景引入
我们先来看这么一个场景:
假设现在有一个社交平台,平台上有一个大V叫Nami
Nami很牛,多才多艺,目前她有2个技能:会写歌、会拍视频
她会把这些作品发布到平台上。关注她的粉丝就会接收到这些内容
现在他已经有3个粉丝了,分别是:Luffy、Zoro、Sanji
每次只要Nami一发布作品,3个粉丝的账号上收到的消息就会更新
现在用代码来表示:
const luffy = {
update: function (songs, videos) {
console.log(songs, videos);
},
};
const zoro = {
update: function (songs, videos) {
console.log(songs, videos);
},
};
const sanji = {
update: function (songs, videos) {
console.log(songs, videos);
},
};
const nami = {
// 只要Nami的作品一更新,这个方法就会被调用
workUpdate: function () {
// 获取作品
const songs = this.getSongs();
const videos = this.getVideos();
// 账号更新
luffy.update(songs, videos);
zoro.update(songs, videos);
sanji.update(songs, videos);
},
getSongs: function () {
return "mp3";
},
getVideos: function () {
return "mp4";
},
};
现在问题来了
- 如果Nami又收获了一个粉丝Robin,我既要添加一个robin对象,又要修改
workUpdate
方法 - 如果Nami又有了一项新技能:写小说,我既要修改
workUpdate
函数,又要修改每个粉丝对象中的update
方法,因为参数增加了一个
发现问题没有?
粉丝对象和大V对象之间的耦合度太高,导致两者很难各自扩展
2 代码优化
2.1 解决增加粉丝问题
先来解决上述第1个问题,使得增加粉丝的时候不用再修改workUpdate
方法
首先,我们将“大V”抽象成一个类Star
,用数组fans
来保存粉丝列表,并新增一个添加粉丝的方法addFans
class Star {
constructor() {
this.fans = [];
}
addFans(fan) {
this.fans.push(fan)
}
workUpdate() {
const songs = this.getSongs();
const videos = this.getVideos();
this.fans.forEach((item) => item.update(songs, videos));
}
getSongs() {
return "MP3";
}
getVideos() {
return "MP4";
}
}
接着,将“粉丝”也抽象成一个类Fan
,我们在创建粉丝对象的时候传入“大V”对象,调用该大V的addFans
方法来添加到粉丝列表
class Fan {
constructor(name, star) {
this.name = name
this.star = star
this.star.addFans(this)
}
update(songs, videos) {
console.log(songs, videos);
}
}
现在我们添加粉丝就不必再更改代码了
const nami = new Star()
const luffy = new Fan("luffy", nami);
const zoro = new Fan("zoro", nami);
const sanji = new Fan("sanji", nami);
const robin = new Fan("robin", nami);
nami.workUpdate()
2.2 解决添加作品问题
我们新增一个works
数组来保存大V的作品,并且为其添加get
和set
方法
class Star {
constructor() {
this.fans = [];
this.works = [];
}
addFans(fan) {
this.fans.push(fan);
}
setWorks(work) {
this.works.push(work);
// 添加作品后,调用更新方法
this.workUpdate();
}
getWorks() {
return this.works;
}
workUpdate() {
this.fans.forEach((item) => item.update());
}
}
对类Fan
进行相应修改:
class Fan {
constructor(name, star) {
this.name = name
this.star = star
this.star.addFans(this)
}
update() {
console.log(`${this.name}:${this.star.getWorks()}`)
}
}
现在大V添加作品就不必再更改代码了:
const nami = new Star();
nami.setWorks('song')
nami.setWorks('video')
nami.setWorks('novel')
const luffy = new Fan("luffy", nami);
const zoro = new Fan("zoro", nami);
const sanji = new Fan("sanji", nami);
nami.workUpdate();
3 观察者模式
可以看到,在上述例子中,一个nami
对象和多个粉丝对象之间存在着一种一对多的依赖关系,当nami
对象有作品更新的时候,所有关注她的粉丝对象都会收到通知。
事实上,这就是观察者模式
观察者模式:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新
我们将2.2
中的代码进行进一步的抽象:
将“粉丝”看作观察者(Observer),将“大V”看作被观察的对象,称为主题(Subject)
Subject
维护一个观察者列表observerList
(原fans
数组)。当Subject
的状态发生变化(原作品更新)时,通过调用notify
(原workUpdate
)方法通知所有观察者,执行它们的update
方法
具体代码如下:
// 被观察者:主题
class Subject {
constructor() {
this.observerList = [];
// 代表主题状态
this.state = 0;
}
addObserver(observer) {
this.observerList.push(observer);
}
// 更改主题状态
setState(state) {
this.state = state;
// 状态改变后,通知所有观察者
this.notify();
}
getState() {
return this.state;
}
notify() {
this.observerList.forEach((observer) => observer.update());
}
}
// 观察者
class Observer {
constructor(name, subject) {
this.name = name;
this.subject = subject;
this.subject.addObserver(this);
}
update() {
console.log(`${this.name}:${this.subject.state}`);
}
}
4 经纪人登场
由于大V业务繁忙,所以他们需要经纪人来维持艺人与粉丝的联系
经纪人的工作包括:
- 维护大V的粉丝,经纪人手中会有一个粉丝名单
- 大V的新作品会交给经纪人,经纪人则负责把新作品发送给粉丝名单中的粉丝
抽象成一个类,如下:
class Manager {
constructor() {
this.fans = [];
this.works = [];
}
addFans(fan) {
this.fans.push(fan);
}
setWorks(work) {
this.works.push(work);
// 添加作品后,调用更新方法
this.workUpdate();
}
getWorks() {
return this.works;
}
workUpdate() {
this.fans.forEach((item) => item.update());
}
}
嗯?这段代码貌似在哪儿见过?
没错,和2.2
的Star
类一模一样,只不过把类名改了改。
那这么做有意义吗?
事实上,代码一模一样是因为在2.2
的Star
类中我们只写了有关发布(即发布作品)和订阅(即维护粉丝列表)的功能;而Star
类本身可能不止这个工作,比如创作内容。
现在我们将Star
类中的发布和订阅的工作抽离出来,交给Manager
全权负责。而Star
类只要在创作完成后把作品交给Manager
就可以了
另一方面,粉丝Fan
也不再直接和Star
发生交互了,Fan
只关心能不能收到作品,所以Fan
直接和Manager
发生交互,Fan
去订阅(这个行为相当于在Manager
维护的粉丝列表中添加粉丝)Manager
并从Manager
那儿获取想要的作品
于是Star
和Fan
的代码如下:
class Star {
constructor() {}
// 创作
create(manager) {
// 将创作的new work交给经纪人
manager.setWorks("new work");
}
}
class Fan {
constructor(name, manager) {
this.name = name;
this.manager = manager;
this.manager.addFans(this);
}
update() {
console.log(`${this.name}:${this.manager.getWorks()}`);
}
}
5 发布订阅模式
前面我们用了经纪人来负责发布和订阅的工作,而不让Star
和Fan
发生直接交互,达到了两者解耦的效果
这就是发布订阅模式
我们将4
中的Manager
进行进一步的抽象:
将“粉丝”看作订阅者(Subscriber);将“大V”看作内容的发布者,在发布订阅模式中称为发布者(Publisher);把“经纪人”看作发布订阅中心(或者说中间人Broker)
具体代码如下:
// 发布订阅调度中心
class Broker {
constructor() {
this.subscribers = [];
// 代表主题状态
this.state = 0;
}
// 订阅
subscribe(subscriber) {
this.subscribers.push(subscriber);
}
// 更改主题状态
setState(state) {
this.state = state;
// 状态改变后,发布
this.publish();
}
getState() {
return this.state;
}
// 发布
publish() {
this.subscribers.forEach((subscriber) => subscriber.update());
}
}
// 发布者
class Publisher {
constructor() {}
changeState(broker, state) {
broker.setState(state);
}
}
class Subscriber {
constructor(name, broker) {
this.name = name;
this.broker = broker;
this.broker.subscribe(this);
}
update() {
console.log(`${this.name}:${this.broker.getState()}`);
}
}
来运行一下看看效果:
// 创建调度中心
const broker = new Broker()
// 创建发布者
const publisher = new Publisher()
// 创建订阅者
const subscribe1 = new Subscriber('s1', broker)
const subscribe2 = new Subscriber('s2', broker)
const subscribe3 = new Subscriber('s3', broker)
// 发布者改变状态并通知调度中心,调度中心就会通知各个订阅者
publisher.changeState(broker, 1)
6 观察者模式和发布订阅模式的对比
从角色数量看
- 观察者模式只有两个角色:观察者和被观察者
- 发布订阅模式有三个角色:发布者、订阅者以及中间人(发布订阅中心)
从耦合程度看
- 观察者模式处于一种松耦合的状态,即两者依然有交互,但是又很容易各自扩展且不相互影响
- 发布订阅模式中的发布者和订阅者则完全不存在耦合,达到了对象之间解耦的效果
从意图来看
- 两者都:实现了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知并自动更新
公众号【前端嘛】
面试官:能用JS写一个发布订阅模式吗?的更多相关文章
- JS中的发布订阅模式
一. 你是如何理解发布订阅模式的 JS中的设计模式: 单例模式:处理业务逻辑 构造原型模式:封装类库,组件,框架,插件等 类库:jQuery 只是提供了一些常用的方法,可以应用到任何的项目中,不具备业 ...
- js里的发布订阅模式及vue里的事件订阅实现
发布订阅模式(观察者模式) 发布订阅模式的定义:它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知. 发布订阅模式在JS中最常见的就是DOM的事件绑定与触发 ...
- js设计模式之发布/订阅模式模式
一.前言 发布订阅模式,基于一个主题/事件通道,希望接收通知的对象(称为subscriber)通过自定义事件订阅主题,被激活事件的对象(称为publisher)通过发布主题事件的方式被通知. 就和用户 ...
- 浅谈js设计模式之发布 — 订阅模式
发布 — 订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知.在 JavaScript开发中,我们一般用事件模型来替代传统的发布 — ...
- js设计模式之发布订阅模式
1. 定义 发布-订阅模式其实是一种对象间一对多的依赖关系,当一个对象的状态发送改变时,所有依赖于它的对象都将得到状态改变的通知. 订阅者(Subscriber)把自己想订阅的事件注册(Subscri ...
- 从发布订阅模式入手读懂Node.js的EventEmitter源码
前面一篇文章setTimeout和setImmediate到底谁先执行,本文让你彻底理解Event Loop详细讲解了浏览器和Node.js的异步API及其底层原理Event Loop.本文会讲一下不 ...
- JS的发布订阅模式
JS的发布订阅模式 这里要说明一下什么是发布-订阅模式 发布-订阅模式里面包含了三个模块,发布者,订阅者和处理中心.这里处理中心相当于报刊办事大厅.发布者相当与某个杂志负责人,他来中心这注册一个的杂志 ...
- Spring源码之七registerListeners()及发布订阅模式
Spring源码之七registerListeners()及发布订阅模式 大家好,我是程序员田同学. 今天带大家解读refresh()方法中的registerListeners()方法,也就是我们经常 ...
- 面试官问你JS基本类型时他想知道什么?
面试的时候我们经常会被问答js的数据类型.大部分情况我们会这样回答包括:1.基本类型(值类型或者原始类型): Number.Boolean.String.NULL.Undefined以及ES6的Sym ...
随机推荐
- Django学习day08随堂笔记
今日考题 """ 今日考题 1.聚合查询,分组查询的关键字各是什么,各有什么特点或者注意事项 2.F与Q查询的功能,他们的导入语句是什么,针对Q有没有其他用法 3.列举常 ...
- 记录不存在则插入,存在则更新 → MySQL 的实现方式有哪些?
开心一刻 今天我爸.我.我女儿一起吃饭,我们每人一个鸡腿 女儿问道:爸爸,你吃鸡腿吗 我以为她要把她的鸡腿给我吃,倍感欣慰地说道:我不吃,宝贝 女儿一把抓起我的鸡腿放进了她爷爷的碗里,说道:不吃给爷爷 ...
- 传说中 VUE 的“语法糖”到底是啥?
一.什么是语法糖? 语法糖也译为糖衣语法,是由英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语.指的是计算机语言中添加的一种语法,在不影响功能的情况下,添加某种简单的语 ...
- vs code安装leetcode插件
vs code 安装不成功啊 1.首先确保有node.js 10+,没有的话去官网下载,安装就可以,安装好之后在cmd命令行中输入: node -v 若出现相关版本信息说明安装成功 2.由于leetc ...
- 安装配置环境 CUDA以及CUDNN tensorflow pytorch pip安装 虚拟环境
1. 在win10中利用Anaconda直接安装tensorflow-gpu 不需要另行安装cuda cudnn 但是不知道电脑会自动适配所需的版本吗,不过把电脑显卡驱动更新一下,就都也可以了吧. ...
- Electron+Vue+ElementUI开发环境搭建
Node环境搭建 本文假定你完成了nodejs的环境基础搭建: 镜像配置(暂时只配置node包镜像源,部分包的二进制镜像源后续讨论).全局以及缓存路径配置,全局路径加入到了环境变量 $ node -v ...
- 题解 Weak in the Middle
题目传送门 Description 有一个长度为 \(n\) 的序列 \(a_{1,2,...,n}\) ,每次可以删掉 \(a_i\),当 \(\min(a_{i-1},a_{i+1})>a_ ...
- 2021.5.22 vj补题
A - Marks CodeForces - 152A 题意:给出一个学生人数n,每个学生的m个学科成绩(成绩从1到9)没有空格排列给出.在每科中都有成绩最好的人或者并列,求出最好成绩的人数 思路:求 ...
- 面试官:Java从编译到执行,发生了什么?
面试官:今天从基础先问起吧,你是怎么理解Java是一门「跨平台」的语言,也就是「一次编译,到处运行的」? 候选者:很好理解啊,因为我们有JVM. 候选者:Java源代码会被编译为class文件,cla ...
- NOIP模拟79
T1 F 解题思路 因为每个点会产生贡献当且仅当它在可以到他的点之前被删除,并且此题遵守期望的线性性. 因此设所有可以到达点 \(i\) 的数量为 \(c_i\) 那么答案就是 \(\sum \fra ...