聊一聊观察者模式

今天和你们来聊一下观察者模式,观察者模式在咱们编程的过程当中很是经常使用,关于编程的模式,个人我的的理解是代码写多了以后,提炼总结出来的一套经验、方法。

那什么是观察者模式呢?笔者就这个问题查了三本书,分别是如图:



在查完资料以后,得出以下结论(三本书中都有提到),观察者模式的另一种名称叫作发布订阅者模式(本文中的观察者模式和订阅者模式指的是一个东西)。观察者模式定义了一种依赖关系,当某一个对象的状态发生变化,其它依赖这个对象的对象都会受到影响。

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



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

这个场景用代码来展现的话以下:


// 初版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,这个属性一般是一个数组,用来存储观察者或者观察者的行为。

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


// 第二版// 一、thief增长了list属性,是一个数组// 二、subscrible方法,追加方法// 三、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// 一、thief增长了list属性,是一个数组// 二、subscrible方法,追加方法// 三、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)// 增长代码: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.length;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对象,而后分别在第3五、3九、43行调用thief1的subsctible方法收集dog的call方法。

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


class Pubsub{ constructor(){ this.list = [] } subscrible(call){ this.list.push(call) } publish(){ for(let i= 0 ;i<this.list.length;i++){ this.list[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内部的代码,实现了对象之间的解耦。

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

第三版代码,咱们能够用以下图示来理解:



观察上图,第三版中图片第一张图多了一个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')// 一、获取CancelTokenvar CancelToken = axios.CancelToken;// 二、生成sourcevar source = CancelToken.source();console.log(source.token)axios.get('/user/12345', {//get请求在第二个参数 // 三、注入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)});// 四、调用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来实现的。

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

一、首先咱们了解了什么是观察者模式,也叫作订阅发布者模式。
二、咱们用thief和dog的案例来演示如何使用观察者模式。
三、咱们根据观察者的特征,将其抽离出来,抽离成一个类,这个类具备一个list属性,用来存储观察者的行为,一个subscrible方法来追加方法,将方法追加到list数组中,一个public方法,用来发布消息,遍历执行list中的函数。
四、咱们讲解了如何用咱们封装出来的pubsub来解耦htief和dog的调用关系,是代码易于维护。
五、根据promise的特性咱们用promise改写了pubsub的代码,用promise的then来存储观察者的行为,用这个promsie的resolve来实现public,这里面咱们演示了如何获取promise.then执行的控制权。
六、而后咱们填了一个坑,讲解了如何用promise实现的观察者实现axios的取消异步回调的功能,本质就是运用了观察者模式,而且是用promsie实现的观察者模式。

终于写完了,若是你有什么疑问或者建议欢迎留言。

本文分享自微信公众号 - nodejs全栈开发(geekclass)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。前端

相关文章
相关标签/搜索