精选文章 JS设计模式之发布订阅者模式

JS设计模式之发布订阅者模式

作者:健超还在敲代码 时间: 2020-08-04 09:58:31
健超还在敲代码 2020-08-04 09:58:31

(本篇文章是我在早高峰上班地铁上刷到了一篇公众号发布的文章,感觉讲的非常好,特分享给大家。每天坚持学习,感谢公众号大佬的分享,公众号:nodejs全栈开发)

首先我们用一个示例来演示一下什么是观察者模式,有这样一个场景,在一个院子里,有一个小偷,和若干条狗,小偷只要一行动,狗就会叫,狗叫的动作是依赖小偷的,如果小偷不行动,狗是不会叫的,也就是说狗的叫的状态依赖小偷的行动,小偷的行动状态发生变化,依赖小偷的狗都会受到影响,从而发出叫声。

这个场景用代码来展示的话如下:

// 第一版
class Thief {
    constructor(){

    }
    // thief的方法,调用dog的方法;
    action(){
        dog1.call()
        dog2.call()
        dog3.call()
    }
}

class Dog {
    call(){
        console.log("狗叫")
    }
}

let dog1 = new Dog()
let dog2 = new Dog()
let dog3 = new Dog()
let thief = new Thief();
thief.action()

上面的代码中,小偷调用action方法的时候,其内部会分别调用每条狗的call方法。这段代码有个明显的缺点,对象耦合,不方便维护,假如需求中增加了一条狗,此时如何更改代码呢?代码如下:

// 第一版-新增dog4
class Thief {
    constructor() {

    }
    // thief的方法,调用dog的方法;
    action() {
        dog1.call()
        dog2.call()
        dog3.call()
        // 新增代码
        dog4.call()
    }
}

class Dog {
    call() {
        console.log("狗叫")
    }
}

let dog1 = new Dog()
let dog2 = new Dog()
let dog3 = new Dog()
// 新增代码:
let dog4 = new Dog()

let thief = new Thief();
thief.action()

观察代码,我们增加了dog4,然后在小偷的action方法中,再增加don4.call的调用,对象间存在了互相调用的耦合,这样的代码非常不便于后期维护,因为每次增加dog都需要去更改thief的代码。

那有没有另外一种代码的书写方式,增加dog,但是不修改thief的代码,同样达到上面的效果呢?

下面我们用观察者模式来改写这段代码,在改写之前,先来了解一下观察者模式的特点,我们再次回顾一下文中对观察者模式的介绍:"观察者模式定义了一种依赖关系,当某一个对象的状态发生变化,其它依赖这个对象的对象都会受到影响"。

仔细阅读我们发现观察者模式中一般会存在观察者和被观察者,通常被观察者是少数一方(并不固定,为了方便先这样理解)。

上面的例子中,小偷是少数一方,只有一个。小偷明显是被观察者,狗是观察者,被观察者通常会有两个方法和一个属性,一个方法叫做subscribe,这个方法用来收集观察者或者观察者的行为,另外一个方法叫做publish,用来发布消息,还有一个属性list,这个属性通常是一个数组,用来存储观察者或者观察者的行为。

下面我们用观察者模式来改写上面的代码,代码如下:

// 第二版
// 1、thief增加了list属性,是一个数组
// 2、subscrible方法,追加方法
// 3、publish 发布消息
class Thief {
    constructor() {
        this.list = []
    }
    // 
    subscrible(call) {
        this.list.push(call)
    }
    // publish遍历数组,调用所有方法。
    publish() {
        for (let i = 0; i < this.list.length; i++) {
            this.list[i]()
        }
    }
    // thief的方法内部不会直接调用dog的方法了,
    // 而是调用publish
    action() {
        this.publish()
    }
}
class Dog {
    call() {
        console.log("狗叫")
    }
}

let thief = new Thief();
let dog1 = new Dog()
thief.subscrible(dog1.call)
// 每增加一条狗就将狗的call方法追加到list

let dog2 = new Dog()
thief.subscrible(dog2.call)
let dog3 = new Dog()
thief.subscrible(dog3.call)
thief.action()

仔细阅读代码,我们首先重新定义了Thief类,并为其添加了subscribe方法、publish方法、list属性,并重新定义了dog。然后我们用thief的subscribe方法收集dog的call方法,将其添加到小偷的list属性中。当小偷调用action时,其内部调用publish方法,publish会遍历执行list数组中的方法。

这段代码相较于上一段代码就比较方便维护了,假如我们在这个基础上再添加一条狗,代码如下:

// 第二版,新增dog4
// 1、thief增加了list属性,是一个数组
// 2、subscrible方法,追加方法
// 3、publish 发布消息
class Thief {
    constructor() {
        this.list = []
    }
    // 
    subscrible(call){
        this.list.push(call)
    }
    // publish遍历数组,调用所有方法。
    publish(){
        for(let i= 0 ;i

我们看到,代码中第41行增加dog4,然后调用thief的scrible收集狗的call方法,此时我们调用thief的publish方法,依然能调用所有dog的call方法,但是我们没有修改thief内部的代码,非常优雅的完成了需求,但是如果需求是再增加一个小偷呢?此时代码是什么样的呢?代码如下:

// 第二版,新增thief
class Thief {
    constructor() {
        this.list = []
    }
    // 
    subscrible(call){
        this.list.push(call)
    }
    // publish遍历数组,调用所有方法。
    publish(){
        for(let i= 0 ;i

看看代码,我们在第30行新增了thief1对象,然后分别在第35、39、43行调用thief1的subsctible方法收集dog的call方法。

真是按下葫芦起了瓢,能不能继续优化呢,在使用观察者模式的时候,我们可以将观察者模式抽离出来,抽离成一个pubsub对象,这个对象有拥有两个方法一个属性,代码如下:

class Pubsub{
    constructor(){
        this.list = []
    }
    subscrible(call){
        this.list.push(call)
    }
    publish(){
        for(let i= 0 ;i

仔细阅读源码,我们只是将观察者的一个属性和两个方法抽离出来封装成了一个类,使用这个类时,实例化一下就可以了,然后用这个对象改写上面的代码: 

 

let pubsub = new Pubsub();
class Dog {
    call() {
        console.log("狗叫")
    }
}

class Thief {
    constructor() {

    }
    action() {
        pubsub.publish()
    }
}

let thief = new Thief();
let dog1 = new Dog()
pubsub.subscrible(dog1.call)
let dog2 = new Dog()
pubsub.subscrible(dog2.call)
let dog3 = new Dog()
pubsub.subscrible(dog3.call)

thief.action()

观察代码,小偷在调用action时,不是直接调用狗的call方法,而是通过pubsub,并且收集狗的call方法,也是由pubsub来完成,完全将小偷和狗解耦了。然后我们在添加一个dog4和一个thief1,代码如下:  

let pubsub = new Pubsub();
class Dog {
    call() {
        console.log("狗叫")
    }
}

class Thief {
    constructor() {

    }
    action() {
        pubsub.publish()
    }
}

let thief = new Thief();

// 新增thief1代码
let thief1 = new Thief();

let dog1 = new Dog()
pubsub.subscrible(dog1.call)
let dog2 = new Dog()
pubsub.subscrible(dog2.call)
let dog3 = new Dog()
pubsub.subscrible(dog3.call)

// 新增dog4代码
let dog4 = new Dog()
pubsub.subscrible(dog4.call)

thief.action()

 

仔细阅读源码,第20行和第30行分别添加了thief1和dog4,依然能够实现小偷偷东西,狗会叫的功能,并且不会去修改thief和dog内部的代码,实现了对象之间的解耦。

 

观察者模式也可以叫做订阅发布模式,本质是一种消息机制,用这种机制我们可以解耦代码中对象互相调用。

 

第三版代码,我们可以用如下图示来理解:

JS设计模式之发布订阅者模式1

 

观察上图,第三版中图片第一张图多了一个pubsub,我们用一个卫星来代替pubsub,这个版本也比较好维护,添加删除thief或者dog都不会影响到对象。我们在前端应用中使用的redux和vuex都运用了观察者模式,或者叫做订阅者模式,其运行原理也如上图。

 

文章写到这里,观察者模式基本就聊完了,但是我在观察pubsub这个对象的时候突然想到了promsie,promise天生就是观察者模式,我们可以用promise来改造一下pubsub,代码如下:

class Pubsub {    constructor() {        let promise = new Promise((resolve,reject)=>{            this.resolve = resolve;        })        this.promise = promise;    }    subscrible(call) {       this.promise.then(call)    }    publish() {        this.resolve();    }}

 

Promise天然支持观察者模式,我们将其改造一下,改造成一个Pubsub类,与我们前面实现的Pubsub类效果是一样的。

 

首先我们在构造函数内部实例化一个promise,并且将这个promsie的resolve的控制权转交到this的resolve属性上。前面写过一篇文章如何取消promise的调用,在这篇文章中我们介绍了如何获取promise的控制权。大家有兴趣可以去看一看。

 

回归正题,我们用promise改写的pubsub来测试下上面的案例,代码如下:

class Pubsub {    constructor() {        let promise = new Promise((resolve,reject)=>{            this.resolve = resolve;        })        this.promise = promise;    }    subscrible(call) {       this.promise.then(call)    }    publish() {        this.resolve();    }}
let pubsub = new Pubsub();class Dog {    call() {        console.log("狗叫")    }}
class Thief {    constructor() {
    }    action() {        pubsub.publish()    }}
let thief = new Thief();
// 新增thief1代码let thief1 = new Thief();
let dog1 = new Dog()pubsub.subscrible(dog1.call)let dog2 = new Dog()pubsub.subscrible(dog2.call)let dog3 = new Dog()pubsub.subscrible(dog3.call)
// 新增dog4代码let dog4 = new Dog()pubsub.subscrible(dog4.call)
thief.action()

 

测试代码,我们发现用promise改造的pubsub也能很好的实现观察者模式,这里我们利用了promise的两个知识点,一个是promise的then方法,then方法可以无限追加函数。另外一个是我们得到promise的resolve的控制权,从而控制promise的then链的执行时机。

 

讲到这里填一下前面文章挖的坑,前面的如何取消ajax请求的回调的文章中我们留了一个坑,axios实现取消ajax请求的回调的原理,我们可以回顾下使用axios时如何取消回调,代码如下:

const axios = require('axios')// 1、获取CancelTokenvar CancelToken = axios.CancelToken;// 2、生成sourcevar source = CancelToken.source();console.log(source.token)axios.get('/user/12345', {//get请求在第二个参数    // 3、注入source.token    cancelToken: source.token}).catch(function (thrown) {    console.log(thrown)});axios.post('/user/12345', {//post请求在第三个参数    name: 'new name'}, {    cancelToken: source.token}).catch(e => {    console.log(e)});// 4、调用source.cancel("原因"),终止注入了source.token的请求source.cancel('不想请求了');

 

阅读代码,在第一步和第二步中,我们通过调用axios.CancelToken.source方法得到了一个source对象,第三步中我们在axios调用异步请求时传递cancelToken参数,第四步,在合适的时机调用source.cancle方法取消回调。

 

我们先看一下CancelToken这个静态方法的代码是如何的:​​​​​​​

'use strict';var Cancel = require('./Cancel');/** * A `CancelToken` is an object that can be used to request cancellation of an operation. * * @class * @param {Function} executor The executor function. */function CancelToken(executor) {    if (typeof executor !== 'function') {        throw new TypeError('executor must be a function.');    }    var resolvePromise;    this.promise = new Promise(function promiseExecutor(resolve) {        resolvePromise = resolve;    });    var token = this;    executor(function cancel(message) {        if (token.reason) {            // Cancellation has already been requested            return;        }        token.reason = new Cancel(message);        resolvePromise(token.reason);    });}/** * Throws a `Cancel` if cancellation has been requested. */CancelToken.prototype.throwIfRequested = function throwIfRequested() {    if (this.reason) {        throw this.reason;    }};/** * Returns an object that contains a new `CancelToken` and a function that, when called, * cancels the `CancelToken`. */CancelToken.source = function source() {    var cancel;    var token = new CancelToken(function executor(c) {        cancel = c;    });    return {        token: token,        cancel: cancel    };};module.exports = CancelToken;

 

为了直观一些我们将注释和一些基础条件判断去除后,代码如下:

function CancelToken(executor) {
    var resolvePromise;    this.promise = new Promise(function promiseExecutor(resolve) {        resolvePromise = resolve;    });    var token = this;    executor(function cancel(message) {        if (token.reason) {            return;        }        token.reason = message        resolvePromise(token.reason);    });}
CancelToken.source = function source() {    var cancel;    var token = new CancelToken(function executor(c) {        cancel = c;    });    return {        token: token,        cancel: cancel    };};

 

阅读源码,我们发现CancelToken是一个类,其构造函数需要传递一个参数,这个参数必须是一个函数,CancelToken通过调用source方法来实例化一个对象。

 

在CancelToken的构造函数中,实例化一个Promise对象,通过在Promise的外部定义ResolvePromise变量,值实例化promise的时候获取了Promise实例resolve的控制权,然后将控制权封装到cancel函数中,在将cancel函数交给CancelToken构造函数的参数executor函数。

 

CancelToken在调用cancel方法时,先实例化CancelToken,在实例化过程中,我们将cancel交给了变量cancel,最后将CancelToken的实例token和cancel方法返回出去。

 

token的实质就是一个promise对象,而cancel方法内部则保存了这个promise的resolve方法。所有我们可以通过cancel来控制promise对象的执行。

 

接着我们再看一下axios中配置cancelToken参数的核心代码:

if (config.cancelToken) {    // Handle cancellation    config.cancelToken.promise.then(function onCanceled(cancel) {        if (!request) {            return;        }        request.abort();        reject(cancel);        // Clean up request        request = null;    });}

 

阅读源码,我们发现,当axios发送异步请求配置了acncelToken参数后,axios内部会执行一段代码:

config.cancelToken.promise.then(function onCanceled(cancel) {    if (!request) {        return;    }    request.abort();    reject(cancel);    // Clean up request    request = null;});

 

这段代码会调用传入的axios的cancelToken的promise.then的执行,但是这个promise.then的执行的控制权在cancel函数中,如果我们在这个异步请求的返回前,我们调用了cancle函数就会执行promise.then从而执行request.abort来取消回调。

 

axios取消异步回调的原理涉及到了两个知识点,首先是利用了xmlhttprequest的abort方法修改readystate的值,其次利用了观察值模式,只不过这个观察者模式用的是promise来实现的。

 

好了行文至此,终于结束了,来总结一下:

 

1、首先我们了解了什么是观察者模式,也叫做订阅发布者模式。

2、我们用thief和dog的案例来演示如何使用观察者模式。

3、我们根据观察者的特征,将其抽离出来,抽离成一个类,这个类具有一个list属性,用来存储观察者的行为,一个subscrible方法来追加方法,将方法追加到list数组中,一个public方法,用来发布消息,遍历执行list中的函数。

4、我们讲解了如何用我们封装出来的pubsub来解耦htief和dog的调用关系,是代码易于维护。

5、根据promise的特性我们用promise改写了pubsub的代码,用promise的then来存储观察者的行为,用这个promsie的resolve来实现public,这里面我们演示了如何获取promise.then执行的控制权。

6、然后我们填了一个坑,讲解了如何用promise实现的观察者实现axios的取消异步回调的功能,本质就是运用了观察者模式,并且是用promsie实现的观察者模式。

勿删,copyright占位
分享文章到微博
分享文章到朋友圈

上一篇:Znode中的存在类型

下一篇:第05章 函数

您可能感兴趣

  • Web端即时通讯实践干货:如何让WebSocket断网重连更快速?

    本文作者网易智慧企业web前端开发工程师马莹莹。为了提升内容质量,收录时有修订和改动。 1、引言 在一个完善的即时通讯IM应用中,WebSocket是极其关键的一环,它为基于Web的即时通讯应用提供了一种全双工的通信机制。但为了提升IM等实际应用场景下的消息即时性和可靠性,我们需要克服WebSocket及其底层依赖的TCP连接对于复杂网络情况下的不稳定性,即时通讯的开发者们通常都需要为其设计...

  • 工作流学习2(书本)

    1、流程引擎的创建。 1.1、ProcessEngineConfiguration的buildProcessEngine方法 使用ProcessEngineConfiguration的create方法可以得到ProcessEngineConfiguration的实例。ProcessEngineConfiguration中提供了一个buildProcessEngine方法,该方法返回一个Pro...

  • 物联网大数据平台有哪些功能特点

      大数据技术是指从各种各样海量类型的数据中,快速获得有价值信息的能力。适用于大数据的技术,包括大规模并行处理(MPP)数据库,数据挖掘电网,分布式文件系统,分布式数据库,云计算平台,互联网,和可扩展的存储系统。   一个物联网大数据平台需要具备哪些功能?与通用的大数据平台相比,它需要具备什么样的特征呢?我们来仔细分析一下。   1.高效分布式   必须是高效的分布式系统。物联网产生的数据量...

  • 历时两周,将我司的Hadoop2升级到Hadoop3,踩了几个大坑...

    文末有赠书福利 继一次超万亿规模的Hadoop NameNode性能故障排查过程之后,虽然解决了Hadoop2.6.0版本在项目中的问题,但客户依然比较担心,一是担心版本过老,还存在其他未发现的问题;二是按目前每天近千亿条的数据增长,终究会遇到NameNode的第二次瓶颈。 基于上述原因,我们决定将当前集群由Hadoop2.6.0版本升级到Hadoop3.2.1版本,且启用联邦模式。历时2周...

  • 论文|从DSSM语义匹配到Google的双塔深度模型召回和广告场景中的双塔模型思考...

    点击标题下「搜索与推荐Wiki」可快速关注 ▼ 相关推荐 ▼ 1、基于DNN的推荐算法介绍 2、传统机器学习和前沿深度学习推荐模型演化关系 3、论文|AGREE-基于注意力机制的群组推荐(附代码) 4、论文|被“玩烂”了的协同过滤加上神经网络怎么搞? 本文包含(文章较长,建议先收藏再阅读,点击文末的阅读原文,查看更多推荐相关文章): DSSM DSSM的变种 MV-DNN Google Tw...

  • 芯片破壁者(十.上):风起樱花之地

    在不断升级的中美科技战中,每个人都很容易发现,在芯片上受制于人似乎是一个最难解的谜题。面对这种情况,很多国人可能都在思考:我们到底有没有可能打破“芯片枷锁”? 而从历史里寻找答案是文明的天性,在审视国家间的半导体博弈时,有一个无法绕开的话题,就是上世纪60年到到90年代,横跨数十年、关系错综复杂的美日半导体纠葛。这段历史中最为人津津乐道的有两点。一是日本在80年代一跃超过美国成为全球半导体产...

  • 一线互联网大厂精选9道Java集合面试题

    作者|码农田小齐|微信公众号 今天这篇文章是单纯的从面试的角度出发,以回答面试题为线索,再把整个 Java 集合框架复习一遍,希望能帮助大家拿下面试。 先上图: 当面试官问问题时,我会先把问题归类,锁定这个知识点在我的知识体系中的位置,然后延展开来想这一块有哪些重点内容,面试官问这个是想考察什么、接下来还想问什么。 这样自己的思路不会混乱,还能预测面试官下一个问题,或者,也可以引导面试官问出...

  • RabbitMQ(一):RabbitMQ简介

    RabbitMQ是目前非常热门的一款消息中间件,不管是互联网大厂还是中小企业都在大量使用。作为一名合格的开发者,有必要对RabbitMQ有所了解,本文是RabbitMQ快速入门文章,主要内容包括RabbitMQ是什么、RabbitMQ核心概念、常用交换器类型等。 RabbitMQ简介 消息队列提供一个异步通信机制,消息的发送者不必一直等待到消息被成功处理才返回,而是立即返回。消息中间件负责处...

华为云40多款云服务产品0元试用活动

免费套餐,马上领取!
CSDN

CSDN

中国开发者社区CSDN (Chinese Software Developer Network) 创立于1999年,致力为中国开发者提供知识传播、在线学习、职业发展等全生命周期服务。