嗯~~~
開(kāi)門見(jiàn)山卸奉,這次我也就不賣關(guān)子了钝诚,今天我們就來(lái)聊一聊 JavasSript 設(shè)計(jì)模式中的 觀察者模式 颖御,首先我們來(lái)認(rèn)識(shí)一下,什么是觀察者模式?
什么是觀察者模式潘拱?
觀察者模式(Observer)
通常又被稱為 發(fā)布-訂閱者模式 或 消息機(jī)制疹鳄,它定義了對(duì)象間的一種一對(duì)多的依賴關(guān)系,只要當(dāng)一個(gè)對(duì)象的狀態(tài)發(fā)生改變時(shí)芦岂,所有依賴于它的對(duì)象都得到通知并被自動(dòng)更新瘪弓,解決了主體對(duì)象與觀察者之間功能的耦合,即一個(gè)對(duì)象狀態(tài)改變給其他對(duì)象通知的問(wèn)題禽最。
單純的看定義腺怯,對(duì)于前端小伙伴們,可能這個(gè)概念還是比較模糊川无,對(duì)于觀察者模式還是一知半解呛占,ok,那我就來(lái)看個(gè)生活中比較貼切的例子懦趋,相信你立馬就懂了~
生活中的觀察者模式
每次小米出新款手機(jī)都是熱銷晾虑,我看中了小米3這款手機(jī),想去小米之家購(gòu)買仅叫,但是到店后售貨員告訴我他們這款手機(jī)很熱銷帜篇,他們已經(jīng)賣完了,現(xiàn)在沒(méi)有貨了诫咱,那我不可能每天都跑過(guò)來(lái)問(wèn)問(wèn)吧笙隙,這樣很耽誤時(shí)間的,于是我將我的手機(jī)號(hào)碼留給銷售小姐姐坎缭,如果他們店里有貨逃沿,讓她打電話通知我就好了,這樣就不用擔(dān)心不知道什么時(shí)候有貨幻锁,也不需要天天跑去問(wèn)了凯亮,如果你已經(jīng)成功買到了手機(jī)呢,那么銷售小姐姐之后也就不需要通知你了~
這樣是不是清晰了很多~諸如此類的案例還有很多哄尔,我也就不在贅述了假消。
觀察者模式的使用
不瞞你說(shuō),我敢保證岭接,過(guò)來(lái)看的每個(gè)人都使用過(guò)觀察者模式~
什么富拗,你不信?
那么來(lái)看看下面這段代碼~
document.querySelector('#btn').addEventListener('click',function () {
alert('You click this btn');
},false)
怎么樣鸣戴,是不是很眼熟啃沪!
沒(méi)錯(cuò),我們平時(shí)對(duì) DOM
的事件綁定就是一個(gè)非常典型的 發(fā)布-訂閱者模式 窄锅,這里我們需要監(jiān)聽(tīng)用戶點(diǎn)擊按鈕這個(gè)動(dòng)作创千,但是我們卻無(wú)法知道用戶什么時(shí)候去點(diǎn)擊,所以我們訂閱 按鈕上的 click
事件,只要按鈕被點(diǎn)擊時(shí)追驴,那么按鈕就會(huì)向訂閱者發(fā)布這個(gè)消息械哟,我們就可以做對(duì)應(yīng)的操作了。
除了我們常見(jiàn)的 DOM
事件綁定外殿雪,觀察者模式應(yīng)用的范圍還有很多~
比如比較當(dāng)下熱門 vue 框架暇咆,里面不少地方都涉及到了觀察者模式,比如:
數(shù)據(jù)的雙向綁定
利用 Object.defineProperty()
對(duì)數(shù)據(jù)進(jìn)行劫持丙曙,設(shè)置一個(gè)監(jiān)聽(tīng)器 Observer
爸业,用來(lái)監(jiān)聽(tīng)所有屬性,如果屬性上發(fā)上變化了亏镰,就需要告訴訂閱者 Watcher
去更新數(shù)據(jù)沃呢,最后指令解析器 Compile
解析對(duì)應(yīng)的指令,進(jìn)而會(huì)執(zhí)行對(duì)應(yīng)的更新函數(shù)拆挥,從而更新視圖薄霜,實(shí)現(xiàn)了雙向綁定~
子組件與父組件通信
Vue
中我們通過(guò) props
完成父組件向子組件傳遞數(shù)據(jù),子組件與父組件通信我們通過(guò)自定義事件即 $on
,$emit
來(lái)實(shí)現(xiàn)纸兔,其實(shí)也就是通過(guò) $emit
來(lái)發(fā)布消息惰瓜,并對(duì)訂閱者 $on
做統(tǒng)一處理 ~
ok,說(shuō)了這么多汉矿,該我們自己露一手了崎坊,接下來(lái)我們來(lái)自己創(chuàng)建一個(gè)簡(jiǎn)單的觀察者~
創(chuàng)建一個(gè)觀察者
首先我們需要?jiǎng)?chuàng)建一個(gè)觀察者對(duì)象,它包含一個(gè)消息容器和三個(gè)方法洲拇,分別是訂閱消息方法 on
, 取消訂閱消息方法 off
奈揍,發(fā)送訂閱消息 subscribe
。
const Observe = (function () {
//防止消息隊(duì)列暴露而被篡改赋续,將消息容器設(shè)置為私有變量
let __message = {};
return {
//注冊(cè)消息接口
on : function () {},
//發(fā)布消息接口
subscribe : function () {},
//移除消息接口
off : function () {}
}
})();
好的男翰,我們的觀察者雛形已經(jīng)出來(lái)了,剩下的就是完善里面的三個(gè)方法~
注冊(cè)消息方法
注冊(cè)消息方法的作用是將訂閱者注冊(cè)的消息推入到消息隊(duì)列中纽乱,因此需要傳遞兩個(gè)參數(shù):消息類型和對(duì)應(yīng)的處理函數(shù)蛾绎,要注意的是,如果推入到消息隊(duì)列是如果此消息不存在鸦列,則要?jiǎng)?chuàng)建一個(gè)該消息類型并將該消息放入消息隊(duì)列中租冠,如果此消息已經(jīng)存在則將對(duì)應(yīng)的方法突入到執(zhí)行方法隊(duì)列中。
//注冊(cè)消息接口
on: function (type, fn) {
//如果此消息不存在薯嗤,創(chuàng)建一個(gè)該消息類型
if( typeof __message[type] === 'undefined' ){
// 將執(zhí)行方法推入該消息對(duì)應(yīng)的執(zhí)行隊(duì)列中
__message[type] = [fn];
}else{
//如果此消息存在顽爹,直接將執(zhí)行方法推入該消息對(duì)應(yīng)的執(zhí)行隊(duì)列中
__message[type].push(fn);
}
}
發(fā)布消息方法
發(fā)布消息,其功能就是當(dāng)觀察者發(fā)布一個(gè)消息是將所有訂閱者訂閱的消息依次執(zhí)行骆姐,也需要傳兩個(gè)參數(shù)镜粤,分別是消息類型和對(duì)應(yīng)執(zhí)行函數(shù)時(shí)所需要的參數(shù)捏题,其中消息類型是必須的。
//發(fā)布消息接口
subscribe: function (type, args) {
//如果該消息沒(méi)有注冊(cè)繁仁,直接返回
if ( !__message[type] ) return;
//定義消息信息
let events = {
type: type, //消息類型
args: args || {} //參數(shù)
},
i = 0, // 循環(huán)變量
len = __message[type].length; // 執(zhí)行隊(duì)列長(zhǎng)度
//遍歷執(zhí)行函數(shù)
for ( ; i < len; i++ ) {
//依次執(zhí)行注冊(cè)消息對(duì)應(yīng)的方法
__message[type][i].call(this,events)
}
}
移除消息方法
移除消息方法涉馅,其功能就是講訂閱者注銷的消息從消息隊(duì)列中清除归园,也需要傳遞消息類型和執(zhí)行隊(duì)列中的某一函數(shù)兩個(gè)參數(shù)黄虱。這里為了避免刪除是,消息不存在的情況庸诱,所以要對(duì)其消息存在性制作校驗(yàn)捻浦。
//移除消息接口
off: function (type, fn) {
//如果消息執(zhí)行隊(duì)列存在
if ( __message[type] instanceof Array ) {
// 從最后一條依次遍歷
let i = __message[type].length - 1;
for ( ; i >= 0; i-- ) {
//如果存在改執(zhí)行函數(shù)則移除相應(yīng)的動(dòng)作
__message[type][i] === fn && __message[type].splice(i, 1);
}
}
}
ok,到此桥爽,我們已經(jīng)實(shí)現(xiàn)了一個(gè)基本的觀察者模型朱灿,接著就是我們大顯身手的時(shí)候了~
趕緊拿出來(lái)測(cè)試測(cè)試啊~
大顯身手
首先我們先來(lái)一個(gè)簡(jiǎn)單的測(cè)試,看看我們自己創(chuàng)建的觀察者模式執(zhí)行效果如何钠四?
//訂閱消息
Observe.on('say', function (data) {
console.log(data.args.text);
})
Observe.on('success',function () {
console.log('success')
});
//發(fā)布消息
Observe.subscribe('say', { text : 'hello world' } )
Observe.subscribe('success');
我們?cè)谙㈩愋蜑?say
的消息中注冊(cè)了兩個(gè)方法盗扒,其中有一個(gè)接受參數(shù),另一個(gè)不需要參數(shù)缀去,然后通過(guò) subscribe
發(fā)布 say
和 success
消息侣灶,結(jié)果跟我們預(yù)期的一樣,控制臺(tái)輸出了 hello world
以及 success
~
看缕碎!我們已經(jīng)成功的實(shí)現(xiàn)了我們的觀察者~ 為自己點(diǎn)個(gè)贊吧褥影!
自定義數(shù)據(jù)的雙向綁定
上面說(shuō)到,vue
雙向綁定是數(shù)據(jù)劫持和發(fā)布訂閱做實(shí)現(xiàn)的咏雌,現(xiàn)在我們借助這種思想凡怎,自己來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的數(shù)據(jù)的雙向綁定~
首先當(dāng)然是要有頁(yè)面結(jié)構(gòu)了,這里不講究什么赊抖,我就隨手一碼了~
<div id="app">
<h3>數(shù)據(jù)的雙向綁定</h3>
<div class="cell">
<div class="text" v-text="myText"></div>
<input class="input" type="text" v-model="myText" >
</div>
</div>
相信你已經(jīng)知道了统倒,我們要做到就是 input
標(biāo)簽的輸入,通過(guò) v-text
綁定到類名為 text
的 div
標(biāo)簽上~
首先我們需要?jiǎng)?chuàng)建一個(gè)類氛雪,這里就叫做 myVue
吧檐薯。
class myVue{
constructor (options){
// 傳入的配置參數(shù)
this.options = options;
// 根元素
this.$el = document.querySelector(options.el);
// 數(shù)據(jù)域
this.$data = options.data;
// 保存數(shù)據(jù)model與view相關(guān)的指令,當(dāng)model改變時(shí)注暗,我們會(huì)觸發(fā)其中的指令類更新坛缕,保證view也能實(shí)時(shí)更新
this._directives = {};
// 數(shù)據(jù)劫持,重新定義數(shù)據(jù)的 set 和 get 方法
this._obverse(this.$data);
// 解析器捆昏,解析模板指令赚楚,并將每個(gè)指令對(duì)應(yīng)的節(jié)點(diǎn)綁定更新函數(shù),添加監(jiān)聽(tīng)數(shù)據(jù)的訂閱者骗卜,一旦數(shù)據(jù)有變動(dòng)宠页,收到通知左胞,更新視圖
this._compile(this.$el);
}
}
這里我們定義了 myVue
構(gòu)造函數(shù),并在構(gòu)造方法中進(jìn)行了一些初始化操作举户,上面做了注釋烤宙,這里我就不在贅述,主要來(lái)看里面關(guān)鍵的兩個(gè)方法 _obverse
和 _compile
俭嘁。
首先是 _observe
方法躺枕,他的作用就是處理傳入的 data
,并重新定義 data
的 set
和 get
方法供填,保證我們?cè)?data
發(fā)生變化的時(shí)候能跟蹤到拐云,并發(fā)布通知,主要用到了 Object.defineProperty()
這個(gè)方法近她,對(duì)這個(gè)方法還不太熟悉的小伙伴們叉瘩,請(qǐng)猛戳這里~
_observe
//_obverse 函數(shù),對(duì)data進(jìn)行處理粘捎,重寫(xiě)data的set和get函數(shù)
_obverse(data){
let val ;
//遍歷數(shù)據(jù)
for( let key in data ){
// 判斷是不是屬于自己本身的屬性
if( data.hasOwnProperty(key) ){
this._directives[key] = [];
}
val = data[key];
//遞歸遍歷
if ( typeof val === 'object' ) {
//遞歸遍歷
this._obverse(val);
}
// 初始當(dāng)前數(shù)據(jù)的執(zhí)行隊(duì)列
let _dir = this._directives[key];
//重新定義數(shù)據(jù)的 get 和 set 方法
Object.defineProperty(this.$data,key,{
enumerable: true,
configurable: true,
get: function () {
return val;
},
set: function (newVal) {
if ( val !== newVal ) {
val = newVal;
// 當(dāng) myText 改變時(shí)薇缅,觸發(fā) _directives 中的綁定的Watcher類的更新
_dir.forEach(function (item) {
//調(diào)用自身指令的更新操作
item._update();
})
}
}
})
}
}
上面的代碼也很簡(jiǎn)單,注釋也都很清楚攒磨,不過(guò)有個(gè)問(wèn)題就是泳桦,我在遞歸遍歷數(shù)據(jù)的時(shí)候,偷了個(gè)小懶 --咧纠,這里我只涉及到了一些簡(jiǎn)單的數(shù)據(jù)結(jié)構(gòu)事哭,復(fù)雜的例如循環(huán)引用的這種我沒(méi)有考慮進(jìn)入载矿,大家可以自行補(bǔ)充一下哈~
接著我們來(lái)看看 _compile
這個(gè)方法罩扇,它實(shí)際上是一個(gè)解析器担神,其功能就是解析模板指令,并將每個(gè)指令對(duì)應(yīng)的節(jié)點(diǎn)綁定更新函數(shù)演痒,添加監(jiān)聽(tīng)數(shù)據(jù)的訂閱者亲轨,一旦數(shù)據(jù)有變動(dòng),就收到通知鸟顺,然后去更新視圖變化惦蚊,具體實(shí)現(xiàn)如下:
_compile
_compile(el){
//子元素
let nodes = el.children;
for( let i = 0 ; i < nodes.length ; i++ ){
let node = nodes[i];
// 遞歸對(duì)所有元素進(jìn)行遍歷,并進(jìn)行處理
if( node.children.length ){
this._compile(node);
}
//如果有 v-text 指令 , 監(jiān)控 node的值 并及時(shí)更新
if( node.hasAttribute('v-text')){
let attrValue = node.getAttribute('v-text');
//將指令對(duì)應(yīng)的執(zhí)行方法放入指令集
this._directives[attrValue].push(new Watcher('text',node,this,attrValue,'innerHTML'))
}
//如果有 v-model屬性讯嫂,并且元素是INPUT或者TEXTAREA蹦锋,我們監(jiān)聽(tīng)它的input事件
if( node.hasAttribute('v-model') && ( node.tagName === 'INPUT' || node.tagName === 'TEXTAREA')){
let _this = this;
//添加input時(shí)間
node.addEventListener('input',(function(){
let attrValue = node.getAttribute('v-model');
//初始化賦值
_this._directives[attrValue].push(new Watcher('input',node,_this,attrValue,'value'));
return function () {
//后面每次都會(huì)更新
_this.$data[attrValue] = node.value;
}
})())
}
}
}
上面的代碼也很清晰,我們從根元素 #app
開(kāi)始遞歸遍歷每個(gè)節(jié)點(diǎn)欧芽,并判斷每個(gè)節(jié)點(diǎn)是否有對(duì)應(yīng)的指令莉掂,這里我們只針對(duì) v-text
和 v-model
,我們對(duì) v-text
進(jìn)行了一次 new Watcher()
千扔,并把它放到了 myText
的指令集里面憎妙,對(duì) v-model
也進(jìn)行了解析库正,對(duì)其所在的 input
綁定了 input
事件,并將其通過(guò) new Watcher()
與 myText
關(guān)聯(lián)起來(lái)厘唾,那么我們就應(yīng)該來(lái)看看這個(gè) Watcher
到底是什么褥符?
Watcher
其實(shí)就是訂閱者,是 _observer
和 _compile
之間通信的橋梁用來(lái)綁定更新函數(shù)抚垃,實(shí)現(xiàn)對(duì) DOM
元素的更新
Warcher
class Watcher{
/*
* name 指令名稱喷楣,例如文本節(jié)點(diǎn),該值設(shè)為"text"
* el 指令對(duì)應(yīng)的DOM元素
* vm 指令所屬myVue實(shí)例
* exp 指令對(duì)應(yīng)的值讯柔,本例如"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];
}
}
每次創(chuàng)建 Watcher
的實(shí)例护昧,都會(huì)傳入相應(yīng)的參數(shù)魂迄,也會(huì)進(jìn)行一次 _update
操作,上述的 _compile
中惋耙,我們創(chuàng)建了兩個(gè) Watcher
實(shí)例捣炬,不過(guò)這兩個(gè)對(duì)應(yīng)的 _update
操作不同而已,對(duì)于 div.text
的操作其實(shí)相當(dāng)于 div.innerHTML=h3.innerHTML = this.data.myText
绽榛, 對(duì)于 input
相當(dāng)于 input.value=this.data.myText
, 這樣每次數(shù)據(jù) set
的時(shí)候湿酸,我們會(huì)觸發(fā)兩個(gè) _update
操作,分別更新 div
和 input
中的內(nèi)容~
廢話不多說(shuō)灭美,趕緊測(cè)試一下吧~
先初始化一下~
//創(chuàng)建vue實(shí)例
const app = new myVue({
el : '#app' ,
data : {
myText : 'hello world'
}
})
接著推溃,上圖~
我們順利的實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的雙向綁定,棒棒噠 ~
結(jié)語(yǔ)
現(xiàn)在届腐,是不是已經(jīng)對(duì)觀察者模式有比較深刻的理解了呢铁坎?其實(shí),我這里說(shuō)了這么多犁苏,只是起到了一個(gè)拋磚引玉的作用硬萍,重要的是設(shè)計(jì)思想,要學(xué)會(huì)將這種設(shè)計(jì)思想合理的應(yīng)用到我們實(shí)際的開(kāi)發(fā)過(guò)程中围详,可能過(guò)程會(huì)比較艱難朴乖,但是紙上得來(lái)終覺(jué)淺,絕知此事要躬行啊助赞,大家加油~
哦买羞,對(duì)了,今天 1024 啊 雹食, 大家節(jié)日快樂(lè)哈~