聊一聊觀察者模式

今天和你們來聊一下觀察者模式,觀察者模式在咱們編程的過程當中很是經常使用,關於編程的模式,個人我的的理解是代碼寫多了以後,提煉總結出來的一套經驗、方法。

那什麼是觀察者模式呢?筆者就這個問題查了三本書,分別是如圖:



在查完資料以後,得出以下結論(三本書中都有提到),觀察者模式的另一種名稱叫作發佈訂閱者模式(本文中的觀察者模式和訂閱者模式指的是一個東西)。觀察者模式定義了一種依賴關係,當某一個對象的狀態發生變化,其它依賴這個對象的對象都會受到影響。

下面咱們用一個示例來演示一下什麼是觀察者模式,有這樣一個場景,在一個院子裏,有一個小偷,和若干條狗,小偷只要一行動,狗就會叫,這個場景若是用圖來展現的話如圖: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源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。前端

相關文章
相關標籤/搜索