用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 相关文章推荐
DHTML 中的绝对定位
Nov 26 Javascript
createElement动态创建HTML对象脚本代码
Nov 24 Javascript
Javascript在IE或Firefox下获取鼠标位置的代码
Dec 18 Javascript
JavaScript闭包函数访问外部变量的方法
Aug 27 Javascript
浅谈javascript基础之客户端事件驱动
Jun 10 Javascript
关于JSON与JSONP简单总结
Aug 16 Javascript
利用forever和pm2部署node.js项目过程
May 10 Javascript
原生js获取left值和top值的三种方法
Aug 02 Javascript
微信小程序中post方法与get方法的封装
Sep 26 Javascript
axios的拦截请求与响应方法
Aug 11 Javascript
vue+iview 实现可编辑表格的示例代码
Oct 31 Javascript
利用Promise自定义一个GET请求的函数示例代码
Mar 20 Javascript
浅谈JavaScript浅拷贝和深拷贝
JavaScript严格模式不支持八进制的问题讲解
Javascript使用integrity属性进行安全验证
Nov 07 #Javascript
JavaScript中时间格式化新思路toLocaleString()
Nov 07 #Javascript
JavaScript中isPrototypeOf函数
JavaScript原型链详解
JavaScript组合继承详解
You might like
PHP判断远程url是否有效的几种方法小结
2011/10/08 PHP
解析php curl_setopt 函数的相关应用及介绍
2013/06/17 PHP
php 启动时报错的简单解决方法
2014/01/27 PHP
IE关闭时判断及AJAX注销案例学习
2013/02/18 Javascript
javascript dom追加内容实现示例
2013/09/21 Javascript
鼠标拖动实现DIV排序示例代码
2013/10/14 Javascript
window.onload和$(function(){})的区别介绍
2013/10/30 Javascript
AngularJS 中的指令实践开发指南(一)
2016/03/20 Javascript
JS & JQuery 动态添加 select option
2016/06/08 Javascript
jQuery 3.0 的 setter和getter 模式详解
2016/07/11 Javascript
jQuery购物网页经典制作案例
2016/08/19 Javascript
轻松实现jquery选项卡切换效果
2016/10/10 Javascript
JS闭包与延迟求值用法示例
2016/12/22 Javascript
postman自定义函数实现 时间函数的思路详解
2019/04/17 Javascript
微信小程序自定义tabBar在uni-app的适配详解
2019/09/30 Javascript
js中addEventListener()与removeEventListener()用法案例分析
2020/03/02 Javascript
在Echarts图中给坐标轴加一个标识线markLine
2020/07/20 Javascript
利用JS判断元素是否为数组的方法示例
2021/01/08 Javascript
Python使用scrapy抓取网站sitemap信息的方法
2015/04/08 Python
python 获取网页编码方式实现代码
2017/03/11 Python
python实现浪漫的烟花秀
2019/01/30 Python
Python实现的大数据分析操作系统日志功能示例
2019/02/11 Python
在Python中os.fork()产生子进程的例子
2019/08/08 Python
HTML5进度条特效
2014/12/18 HTML / CSS
GEOX鞋美国官方网站:意大利会呼吸的鞋
2017/07/12 全球购物
澳大利亚在线家具、灯饰和家居装饰店:LivingStyles
2018/11/20 全球购物
Lululemon英国官网:加拿大瑜伽服装品牌
2019/01/14 全球购物
异常和异常类的概念
2014/09/12 面试题
写一个函数,求一个字符串的长度。在main函数中输入字符串,并输出其长度
2015/11/18 面试题
介绍一下linux的文件系统
2015/10/06 面试题
学生周末长期请假条
2014/02/15 职场文书
幼儿园教师的考核评语
2014/04/18 职场文书
副处级干部考察材料
2014/05/17 职场文书
综治维稳工作承诺书
2014/08/30 职场文书
二手房购房协议书范本
2014/10/05 职场文书
2015年学雷锋活动总结
2015/02/06 职场文书