JavaScript設計模式之觀察者模式

嗯~~~前端

開門見山,此次我也就不賣關子了,今天咱們就來聊一聊 JavasSript 設計模式中的 觀察者模式 ,首先咱們來認識一下,什麼是觀察者模式?vue

什麼是觀察者模式?

觀察者模式(Observer)node

一般又被稱爲 發佈-訂閱者模式消息機制,它定義了對象間的一種一對多的依賴關係,只要當一個對象的狀態發生改變時,全部依賴於它的對象都獲得通知並被自動更新,解決了主體對象與觀察者之間功能的耦合,即一個對象狀態改變給其餘對象通知的問題。設計模式

單純的看定義,對於前端小夥伴們,可能這個概念仍是比較模糊,對於觀察者模式仍是隻知其一;不知其二,ok,那我就來看個生活中比較貼切的例子,相信你立馬就懂了~bash

生活中的觀察者模式數據結構

每次小米出新款手機都是熱銷,我看中了小米3這款手機,想去小米之家購買,可是到店後售貨員告訴我他們這款手機很熱銷,他們已經賣完了,如今沒有貨了,那我不可能天天都跑過來問問吧,這樣很耽誤時間的,因而我將個人手機號碼留給銷售小姐姐,若是他們店裏有貨,讓她打電話通知我就行了,這樣就不用擔憂不知道何時有貨,也不須要每天跑去問了,若是你已經成功買到了手機呢,那麼銷售小姐姐以後也就不須要通知你了~併發

這樣是否是清晰了不少~諸如此類的案例還有不少,我也就不在贅述了。app

觀察者模式的使用

不瞞你說,我敢保證,過來看的每一個人都使用過觀察者模式~框架

什麼,你不信?函數

那麼來看看下面這段代碼~

document.querySelector('#btn').addEventListener('click',function () {
        alert('You click this btn');
    },false)
複製代碼

怎麼樣,是否是很眼熟!

沒錯,咱們平時對 DOM 的事件綁定就是一個很是典型的 發佈-訂閱者模式 ,這裏咱們須要監聽用戶點擊按鈕這個動做,可是咱們卻沒法知道用戶何時去點擊,因此咱們訂閱 按鈕上的 click 事件,只要按鈕被點擊時,那麼按鈕就會向訂閱者發佈這個消息,咱們就能夠作對應的操做了。

除了咱們常見的 DOM 事件綁定外,觀察者模式應用的範圍還有不少~

好比比較當下熱門 vue 框架,裏面很多地方都涉及到了觀察者模式,好比:

數據的雙向綁定

利用 Object.defineProperty() 對數據進行劫持,設置一個監聽器 Observer,用來監聽全部屬性,若是屬性上發上變化了,就須要告訴訂閱者 Watcher 去更新數據,最後指令解析器 Compile 解析對應的指令,進而會執行對應的更新函數,從而更新視圖,實現了雙向綁定~

子組件與父組件通訊

Vue 中咱們經過 props 完成父組件向子組件傳遞數據,子組件與父組件通訊咱們經過自定義事件即 $on,$emit來實現,其實也就是經過 $emit 來發布消息,並對訂閱者 $on 作統一處理 ~

ok,說了這麼多,該咱們本身露一手了,接下來咱們來本身建立一個簡單的觀察者~

建立一個觀察者

首先咱們須要建立一個觀察者對象,它包含一個消息容器和三個方法,分別是訂閱消息方法 on , 取消訂閱消息方法 off ,發送訂閱消息 subscribe

const Observe = (function () {
    	//防止消息隊列暴露而被篡改,將消息容器設置爲私有變量
    	let __message = {};
    	return {
        	//註冊消息接口
            on : function () {},
            //發佈消息接口
    		subscribe : function () {},
            //移除消息接口
            off : function () {}
        }
    })();
複製代碼

好的,咱們的觀察者雛形已經出來了,剩下的就是完善裏面的三個方法~

註冊消息方法

註冊消息方法的做用是將訂閱者註冊的消息推入到消息隊列中,所以須要傳遞兩個參數:消息類型和對應的處理函數,要注意的是,若是推入到消息隊列是若是此消息不存在,則要建立一個該消息類型並將該消息放入消息隊列中,若是此消息已經存在則將對應的方法突入到執行方法隊列中。

//註冊消息接口
    on: function (type, fn) {
        //若是此消息不存在,建立一個該消息類型
        if( typeof __message[type] === 'undefined' ){
        	// 將執行方法推入該消息對應的執行隊列中
            __message[type] = [fn];
        }else{
        	//若是此消息存在,直接將執行方法推入該消息對應的執行隊列中
            __message[type].push(fn);
        }
    }
複製代碼

發佈消息方法

發佈消息,其功能就是當觀察者發佈一個消息是將全部訂閱者訂閱的消息依次執行,也須要傳兩個參數,分別是消息類型和對應執行函數時所須要的參數,其中消息類型是必須的。

//發佈消息接口
    subscribe: function (type, args) {
    	//若是該消息沒有註冊,直接返回
    	if ( !__message[type] )  return;
    	//定義消息信息
    	let events = {
        	type: type,           //消息類型
        	args: args || {}       //參數
        },
        i = 0,                         // 循環變量
        len = __message[type].length;   // 執行隊列長度
    	//遍歷執行函數
    	for ( ; i < len; i++ ) {
    		//依次執行註冊消息對應的方法
            __message[type][i].call(this,events)
    	}
    }
複製代碼

移除消息方法

移除消息方法,其功能就是講訂閱者註銷的消息從消息隊列中清除,也須要傳遞消息類型和執行隊列中的某一函數兩個參數。這裏爲了不刪除是,消息不存在的狀況,因此要對其消息存在性製做校驗。

//移除消息接口
    off: function (type, fn) {
    	//若是消息執行隊列存在
    	if ( __message[type] instanceof Array ) {
    		// 從最後一條依次遍歷
    		let i = __message[type].length - 1;
    		for ( ; i >= 0; i-- ) {
    			//若是存在改執行函數則移除相應的動做
    			__message[type][i] === fn && __message[type].splice(i, 1);
    		}
    	}
    }
複製代碼

ok,到此,咱們已經實現了一個基本的觀察者模型,接着就是咱們大顯身手的時候了~ 趕忙拿出來測試測試啊~

大顯身手

首先咱們先來一個簡單的測試,看看咱們本身建立的觀察者模式執行效果如何?

//訂閱消息
    Observe.on('say', function (data) {
    	console.log(data.args.text);
    })
    Observe.on('success',function () {
        console.log('success')
    });
    
    //發佈消息
    Observe.subscribe('say', { text : 'hello world' } )
    Observe.subscribe('success');  
複製代碼

咱們在消息類型爲 say 的消息中註冊了兩個方法,其中有一個接受參數,另外一個不須要參數,而後經過 subscribe 發佈 saysuccess 消息,結果跟咱們預期的同樣,控制檯輸出了 hello world 以及 success ~

看!咱們已經成功的實現了咱們的觀察者~ 爲本身點個贊吧!

自定義數據的雙向綁定

上面說到,vue 雙向綁定是數據劫持和發佈訂閱作實現的,如今咱們藉助這種思想,本身來實現一個簡單的數據的雙向綁定~

首先固然是要有頁面結構了,這裏不講究什麼,我就隨手一碼了~

<div id="app">
    <h3>數據的雙向綁定</h3>
    <div class="cell">
        <div class="text" v-text="myText"></div>
        <input class="input" type="text" v-model="myText" >
    </div>
</div>
複製代碼

相信你已經知道了,咱們要作到就是 input 標籤的輸入,經過 v-text 綁定到類名爲 textdiv 標籤上~

首先咱們須要建立一個類,這裏就叫作 myVue 吧。

class myVue{
    constructor (options){
        // 傳入的配置參數
        this.options = options;
        // 根元素
        this.$el = document.querySelector(options.el);
        // 數據域
        this.$data = options.data;
        
        // 保存數據model與view相關的指令,當model改變時,咱們會觸發其中的指令類更新,保證view也能實時更新
        this._directives = {};
        // 數據劫持,從新定義數據的 set 和 get 方法
        this._obverse(this.$data);
        // 解析器,解析模板指令,並將每一個指令對應的節點綁定更新函數,添加監聽數據的訂閱者,一旦數據有變更,收到通知,更新視圖
        this._compile(this.$el);
    }
}
複製代碼

這裏咱們定義了 myVue 構造函數,並在構造方法中進行了一些初始化操做,上面作了註釋,這裏我就不在贅述,主要來看裏面關鍵的兩個方法 _obverse_compile

首先是 _observe 方法,他的做用就是處理傳入的 data ,並從新定義 datasetget 方法,保證咱們在 data 發生變化的時候能跟蹤到,併發布通知,主要用到了 Object.defineProperty() 這個方法,對這個方法還不太熟悉的小夥伴們,請猛戳這裏~

_observe

//_obverse 函數,對data進行處理,重寫data的set和get函數
    _obverse(data){
    	let val ;
    	//遍歷數據
        for( let key in data ){
            // 判斷是否是屬於本身自己的屬性
            if( data.hasOwnProperty(key) ){
            	this._directives[key] = [];
            }
        
            val = data[key];        
            //遞歸遍歷
            if ( typeof val === 'object' ) {
            	//遞歸遍歷
            	this._obverse(val);
            }
            
            // 初始當前數據的執行隊列
            let _dir = this._directives[key];
        
            //從新定義數據的 get 和 set 方法
            Object.defineProperty(this.$data,key,{
            	enumerable: true,
            	configurable: true,
            	get: function () {
            		return val;
            	},
            	set: function (newVal) {
            		if ( val !== newVal ) {
            			val = newVal;
            			// 當 myText 改變時,觸發 _directives 中的綁定的Watcher類的更新
            			_dir.forEach(function (item) {
            			    //調用自身指令的更新操做
            				item._update();
            			})
            		}
            	}
            })
        }
    }
複製代碼

上面的代碼也很簡單,註釋也都很清楚,不過有個問題就是,我在遞歸遍歷數據的時候,偷了個小懶 --,這裏我只涉及到了一些簡單的數據結構,複雜的例如循環引用的這種我沒有考慮進入,你們能夠自行補充一下哈~

接着咱們來看看 _compile 這個方法,它其實是一個解析器,其功能就是解析模板指令,並將每一個指令對應的節點綁定更新函數,添加監聽數據的訂閱者,一旦數據有變更,就收到通知,而後去更新視圖變化,具體實現以下:

_compile

_compile(el){
    //子元素
    let nodes = el.children;
    for( let i = 0 ;  i < nodes.length ; i++ ){
    	let node = nodes[i];
    	// 遞歸對全部元素進行遍歷,並進行處理
    	if( node.children.length ){
    		this._compile(node);
    	}
    
        //若是有 v-text 指令 , 監控 node的值 並及時更新
        if( node.hasAttribute('v-text')){
            let attrValue = node.getAttribute('v-text');
            //將指令對應的執行方法放入指令集
            this._directives[attrValue].push(new Watcher('text',node,this,attrValue,'innerHTML'))
        }
    
    	//若是有 v-model屬性,而且元素是INPUT或者TEXTAREA,咱們監聽它的input事件
        if( node.hasAttribute('v-model') && ( node.tagName === 'INPUT' || node.tagName === 'TEXTAREA')){
            let _this = this;
            //添加input時間
            node.addEventListener('input',(function(){
            	let attrValue = node.getAttribute('v-model');
            	//初始化賦值
            	_this._directives[attrValue].push(new Watcher('input',node,_this,attrValue,'value'));
                return function () {
                    //後面每次都會更新
                    _this.$data[attrValue] = node.value;
            	}
            })())
        }
    }
}
複製代碼

上面的代碼也很清晰,咱們從根元素 #app 開始遞歸遍歷每一個節點,並判斷每一個節點是否有對應的指令,這裏咱們只針對 v-textv-model,咱們對 v-text 進行了一次 new Watcher(),並把它放到了 myText 的指令集裏面,對 v-model 也進行了解析,對其所在的 input 綁定了 input 事件,並將其經過 new Watcher()myText 關聯起來,那麼咱們就應該來看看這個 Watcher 究竟是什麼?

Watcher 其實就是訂閱者,是 _observer_compile 之間通訊的橋樑用來綁定更新函數,實現對 DOM 元素的更新

Warcher

class Watcher{
    /*
    * name  指令名稱,例如文本節點,該值設爲"text"
    * el    指令對應的DOM元素
    * vm    指令所屬myVue實例
    * exp   指令對應的值,本例如"myText"
    * attr  綁定的屬性值,本例爲"innerHTML"
    * */
    constructor (name, el, vm, exp, attr){
        this.name = name;
        this.el = el;
        this.vm = vm;
        this.exp = exp;
        this.attr = attr;
    
        //更新操做
        this._update();
    }
    
    _update(){
    	this.el[this.attr] = this.vm.$data[this.exp];
    }
}
複製代碼

每次建立 Watcher 的實例,都會傳入相應的參數,也會進行一次 _update 操做,上述的 _compile 中,咱們建立了兩個 Watcher 實例,不過這兩個對應的 _update 操做不一樣而已,對於 div.text 的操做其實至關於 div.innerHTML=h3.innerHTML = this.data.myText , 對於 input 至關於 input.value=this.data.myText , 這樣每次數據 set 的時候,咱們會觸發兩個 _update 操做,分別更新 divinput 中的內容~

廢話很少說,趕忙測試一下吧~

先初始化一下~

//建立vue實例
    const app = new myVue({
        el : '#app' ,
        data : {
            myText : 'hello world'
        }
    })

複製代碼

接着,上圖~

咱們順利的實現了一個簡單的雙向綁定,棒棒噠 ~

結語

如今,是否是已經對觀察者模式有比較深入的理解了呢?其實,我這裏說了這麼多,只是起到了一個拋磚引玉的做用,重要的是設計思想,要學會將這種設計思想合理的應用到咱們實際的開發過程當中,可能過程會比較艱難,可是紙上得來終覺淺,絕知此事要躬行啊,你們加油~

哦,對了,今天 1024 啊 , 你們節日快樂哈~

相關文章
相關標籤/搜索