热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

如何用promsie实现观察者模式

这篇文章主要介绍“如何用promsie实现观察者模式”,在日常操作中,相信很多人在如何用promsie实现观察者模式问题上存在疑惑,小编查阅了各式资料

这篇文章主要介绍“如何用promsie实现观察者模式”,在日常操作中,相信很多人在如何用promsie实现观察者模式问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”如何用promsie实现观察者模式”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

下面我们用一个示例来演示一下什么是观察者模式,有这样一个场景,在一个院子里,有一个小偷,和若干条狗,小偷只要一行动,狗就会叫,这个场景如果用图来展示的话如图:


如何用promsie实现观察者模式

我们看到狗叫的动作是依赖小偷的,如果小偷不行动,狗是不会叫的,也就是说狗的叫的状态依赖小偷的行动,小偷的行动状态发生变化,依赖小偷的狗都会受到影响,从而发出叫声。

这个场景用代码来展示的话如下:
// 第一版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方法。这段代码有个明显的缺点,对象耦合,不方便维护,假如需求中增加了一条狗,此时如何更改代码呢?代码如下:
// 第一版-新增dog4class 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[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            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)// 增加代码:let dog4 = new Dog()thief.subscrible(dog4.call)thief.action()
我们看到,代码中第41行增加dog4,然后调用thief的scrible收集狗的call方法,此时我们调用thief的publish方法,依然能调用所有dog的call方法,但是我们没有修改thief内部的代码,非常优雅的完成了需求,但是如果需求是再增加一个小偷呢?此时代码是什么样的呢?代码如下:
// 第二版,新增thiefclass Thief {    constructor() {        this.list = []    }    //     subscrible(call){        this.list.push(call)    }    // publish遍历数组,调用所有方法。    publish(){        for(let i= 0 ;i            this.list[i]()        }    }    // thief的方法内部不会直接调用dog的方法了,    // 而是调用publish    action() {       this.publish()    }}class Dog {    call() {        console.log("狗叫")    }}
let thief = new Thief();// 新增thief代码let thief1 = new Thief()
let dog1 = new Dog()thief.subscrible(dog1.call)// 新增代码thief1.subscrible(dog1.call)let dog2 = new Dog()thief.subscrible(dog2.call)// 新增代码thief1.subscrible(dog2.call)let dog3 = new Dog()thief.subscrible(dog3.call)// 新增代码thief1.subscrible(dog3.call)
thief.action()// 新增代码thief1.action()
看看代码,我们在第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内部的代码,实现了对象之间的解耦。

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

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

如何用promsie实现观察者模式

观察上图,第三版中图片第一张图多了一个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来实现的。

到此,关于“如何用promsie实现观察者模式”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注编程笔记网站,小编会继续努力为大家带来更多实用的文章!


推荐阅读
  • Python瓦片图下载、合并、绘图、标记的代码示例
    本文提供了Python瓦片图下载、合并、绘图、标记的代码示例,包括下载代码、多线程下载、图像处理等功能。通过参考geoserver,使用PIL、cv2、numpy、gdal、osr等库实现了瓦片图的下载、合并、绘图和标记功能。代码示例详细介绍了各个功能的实现方法,供读者参考使用。 ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • 本文介绍了在Vue项目中如何结合Element UI解决连续上传多张图片及图片编辑的问题。作者强调了在编码前要明确需求和所需要的结果,并详细描述了自己的代码实现过程。 ... [详细]
  • 本文记录了在vue cli 3.x中移除console的一些采坑经验,通过使用uglifyjs-webpack-plugin插件,在vue.config.js中进行相关配置,包括设置minimizer、UglifyJsPlugin和compress等参数,最终成功移除了console。同时,还包括了一些可能出现的报错情况和解决方法。 ... [详细]
  • 本文介绍了如何使用JSONObiect和Gson相关方法实现json数据与kotlin对象的相互转换。首先解释了JSON的概念和数据格式,然后详细介绍了相关API,包括JSONObject和Gson的使用方法。接着讲解了如何将json格式的字符串转换为kotlin对象或List,以及如何将kotlin对象转换为json字符串。最后提到了使用Map封装json对象的特殊情况。文章还对JSON和XML进行了比较,指出了JSON的优势和缺点。 ... [详细]
  • 本文介绍了如何使用PHP向系统日历中添加事件的方法,通过使用PHP技术可以实现自动添加事件的功能,从而实现全局通知系统和迅速记录工具的自动化。同时还提到了系统exchange自带的日历具有同步感的特点,以及使用web技术实现自动添加事件的优势。 ... [详细]
  • vue使用
    关键词: ... [详细]
  • Spring源码解密之默认标签的解析方式分析
    本文分析了Spring源码解密中默认标签的解析方式。通过对命名空间的判断,区分默认命名空间和自定义命名空间,并采用不同的解析方式。其中,bean标签的解析最为复杂和重要。 ... [详细]
  • PHP图片截取方法及应用实例
    本文介绍了使用PHP动态切割JPEG图片的方法,并提供了应用实例,包括截取视频图、提取文章内容中的图片地址、裁切图片等问题。详细介绍了相关的PHP函数和参数的使用,以及图片切割的具体步骤。同时,还提供了一些注意事项和优化建议。通过本文的学习,读者可以掌握PHP图片截取的技巧,实现自己的需求。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • javascript  – 概述在Firefox上无法正常工作
    我试图提出一些自定义大纲,以达到一些Web可访问性建议.但我不能用Firefox制作.这就是它在Chrome上的外观:而那个图标实际上是一个锚点.在Firefox上,它只概述了整个 ... [详细]
  • 本文介绍了Oracle数据库中tnsnames.ora文件的作用和配置方法。tnsnames.ora文件在数据库启动过程中会被读取,用于解析LOCAL_LISTENER,并且与侦听无关。文章还提供了配置LOCAL_LISTENER和1522端口的示例,并展示了listener.ora文件的内容。 ... [详细]
  • Redis底层数据结构之压缩列表的介绍及实现原理
    本文介绍了Redis底层数据结构之压缩列表的概念、实现原理以及使用场景。压缩列表是Redis为了节约内存而开发的一种顺序数据结构,由特殊编码的连续内存块组成。文章详细解释了压缩列表的构成和各个属性的含义,以及如何通过指针来计算表尾节点的地址。压缩列表适用于列表键和哈希键中只包含少量小整数值和短字符串的情况。通过使用压缩列表,可以有效减少内存占用,提升Redis的性能。 ... [详细]
author-avatar
手机用户2502936971
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有