用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 相关文章推荐
Javascript数组的排序 sort()方法和reverse()方法
Jun 04 Javascript
JQuery slideshow的一个小问题(如何发现及解决过程)
Feb 06 Javascript
6款新颖的jQuery和CSS3进度条插件推荐
Mar 05 Javascript
Javascript代码在页面加载时的执行顺序介绍
May 03 Javascript
javascript使用正则表达式实现去掉空格之后的字符
Feb 15 Javascript
AngularJS数据源的多种获取方式汇总
Feb 02 Javascript
BootStrap智能表单实战系列(三)分块表单配置详解
Jun 13 Javascript
Bootstrap入门教程一Hello Bootstrap初识
Mar 02 Javascript
JavaScript字符串检索字符的方法
Jun 23 Javascript
AngularJS+Bootstrap3多级导航菜单的实现代码
Aug 16 Javascript
使用vue如何构建一个自动建站项目
Feb 05 Javascript
vue项目中使用Hbuilder打包app 设置沉浸式状态栏的方法
Oct 22 Javascript
浅谈JavaScript浅拷贝和深拷贝
JavaScript严格模式不支持八进制的问题讲解
Javascript使用integrity属性进行安全验证
Nov 07 #Javascript
JavaScript中时间格式化新思路toLocaleString()
Nov 07 #Javascript
JavaScript中isPrototypeOf函数
JavaScript原型链详解
JavaScript组合继承详解
You might like
PHP+Ajax异步通讯实现用户名邮箱验证是否已注册( 2种方法实现)
2011/12/28 PHP
php多进程并发编程防止出现僵尸进程的方法分析
2020/02/28 PHP
JS获取父节点方法
2009/08/20 Javascript
深入认识javascript中的eval函数
2009/11/02 Javascript
js完美的div拖拽实例代码
2014/01/22 Javascript
jquery操作 iframe的方法
2014/12/03 Javascript
AngularJS基础知识
2014/12/21 Javascript
jQuery性能优化技巧分析
2015/02/20 Javascript
AngularJS中的过滤器使用详解
2015/06/16 Javascript
Jquery技巧(必须掌握)
2016/03/16 Javascript
简单封装js的dom查询实例代码
2016/07/08 Javascript
浅析script标签中的defer与async属性
2016/11/30 Javascript
JS实现选定指定HTML元素对象中指定文本内容功能示例
2017/02/13 Javascript
JavaScript函数柯里化原理与用法分析
2017/03/31 Javascript
实现jquery放大镜的两种方法
2018/02/22 jQuery
实例详解ztree在vue项目中使用并且带有搜索功能
2018/08/24 Javascript
简单说说angular.json文件的使用
2018/10/29 Javascript
vue组件从开发到发布的实现步骤
2018/11/11 Javascript
微信小程序实现通过双向滑动缩放图片大小的方法
2018/12/30 Javascript
JavaScript使用Math.random()生成简单的验证码
2019/01/21 Javascript
[原创]微信小程序获取网络类型的方法示例
2019/03/01 Javascript
node.js基于socket.io快速实现一个实时通讯应用
2019/04/23 Javascript
js实现图片区域可点击大小随意改变(适用移动端)代码实例
2019/09/11 Javascript
使用React-Router实现前端路由鉴权的示例代码
2020/07/26 Javascript
Python爬虫设置代理IP的方法(爬虫技巧)
2018/03/04 Python
Django框架安装方法图文详解
2019/11/04 Python
基于Pytorch SSD模型分析
2020/02/18 Python
python 最简单的实现适配器设计模式的示例
2020/06/30 Python
使用python-cv2实现Harr+Adaboost人脸识别的示例
2020/10/27 Python
利用css3画个同心圆示例代码
2017/07/03 HTML / CSS
详解html5 postMessage解决跨域通信的问题
2018/08/17 HTML / CSS
香港最大的洋酒零售连锁店:屈臣氏酒窖(Watson’s Wine)
2018/12/10 全球购物
大二法学专业职业生涯规划范文
2014/02/12 职场文书
婚内房产协议书范本
2014/10/02 职场文书
《秋思》教学反思
2016/02/23 职场文书
台积电称即便经济低迷也没有降价的计划
2022/04/21 数码科技