用JS写一个发布订阅模式


Posted in Javascript onNovember 07, 2021

什么是发布订阅模式?能手写实现一下吗?它和观察者模式有区别吗?...

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的作品,并且为其添加getset方法

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 发布订阅模式

前面我们用了经纪人来负责发布和订阅的工作,而不让StarFan发生直接交互,达到了两者解耦的效果

这就是发布订阅模式

我们将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写一个发布订阅模式内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
js弹出层(jQuery插件形式附带reLoad功能)
Apr 12 Javascript
JavaScript的继承的封装介绍
Oct 15 Javascript
ext combobox动态加载数据库数据(附前后台)
Jun 17 Javascript
JavaScript节点及列表操作实例小结
Aug 05 Javascript
微信小程序实现多个按钮toggle功能的实例
Jun 13 Javascript
vue结合axios与后端进行ajax交互的方法
Jul 06 Javascript
AngularJS自定义表单验证功能实例详解
Aug 24 Javascript
详解JavaScript的变量
Apr 04 Javascript
详解vue微信网页授权最终解决方案
Jun 16 Javascript
vue中监听返回键问题
Aug 28 Javascript
基于vue+uniapp直播项目实现uni-app仿抖音/陌陌直播室功能
Nov 12 Javascript
vue 微信扫码登录(自定义样式)
Jan 06 Javascript
浅谈JavaScript浅拷贝和深拷贝
JavaScript严格模式不支持八进制的问题讲解
Javascript使用integrity属性进行安全验证
Nov 07 #Javascript
JavaScript中时间格式化新思路toLocaleString()
Nov 07 #Javascript
JavaScript中isPrototypeOf函数
JavaScript原型链详解
JavaScript组合继承详解
You might like
ecshop 订单确认中显示省市地址信息的方法
2010/03/15 PHP
php如何解决无法上传大于8M的文件问题
2014/03/10 PHP
php防止网站被刷新的方法汇总
2014/12/01 PHP
PHP 芝麻信用接入的注意事项
2016/12/01 PHP
asp函数split()对应php函数explode()
2019/02/27 PHP
jquery获取被勾选的checked(选中)的那一行的3列和4列的值
2013/07/04 Javascript
在windows上用nodejs搭建静态文件服务器的简单方法
2016/08/11 NodeJs
vue构建单页面应用实战
2017/04/10 Javascript
微信小程序商城项目之侧栏分类效果(1)
2017/04/17 Javascript
深入理解Javascript中的作用域链和闭包
2017/04/25 Javascript
JavaScript实现简单的星星评分效果
2017/05/18 Javascript
JS滚动到指定位置导航栏固定顶部
2017/07/03 Javascript
Vue实现typeahead组件功能(非常靠谱)
2017/08/26 Javascript
vue内置指令详解
2018/04/03 Javascript
JavaScript事件委托原理与用法实例分析
2018/06/07 Javascript
学习RxJS之JavaScript框架Cycle.js
2019/06/17 Javascript
[01:28:43]2014 DOTA2华西杯精英邀请赛5 24 DK VS CIS
2014/05/25 DOTA
python装饰器使用方法实例
2013/11/21 Python
Python Sleep休眠函数使用简单实例
2015/02/02 Python
python中管道用法入门实例
2015/06/04 Python
python利用Tesseract识别验证码的方法示例
2019/01/21 Python
处理python中多线程与多进程中的数据共享问题
2019/07/28 Python
python selenium爬取斗鱼所有直播房间信息过程详解
2019/08/09 Python
python之pymysql模块简单应用示例代码
2019/12/16 Python
详解python环境安装selenium和手动下载安装selenium的方法
2020/03/17 Python
canvas之自定义头像功能实现代码示例
2017/09/29 HTML / CSS
德国EGOIST网店:销售畅销的设计师品牌
2017/04/18 全球购物
奥地利智能家居和智能生活网上商店:tink.at
2019/10/07 全球购物
试解释COMMIT操作和ROLLBACK操作的语义
2014/07/25 面试题
在网络中有两台主机A和B,并通过路由器和其他交换设备连接起来,已经确认物理连接正确无误,怎么来测试这两台机器是否连通?如果不通,怎么来判断故障点?怎么排
2014/01/13 面试题
修理厂厂长岗位职责
2014/01/30 职场文书
先进个人总结范文
2015/02/15 职场文书
家庭暴力离婚起诉书
2015/05/18 职场文书
转变工作作风心得体会
2016/01/23 职场文书
浅谈Redis主从复制以及主从复制原理
2021/05/29 Redis
gateway与spring-boot-starter-web冲突问题的解决
2021/07/16 Java/Android