嗯~~~前端
开门见山,此次我也就不卖关子了,今天咱们就来聊一聊 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
发布 say
和 success
消息,结果跟咱们预期的同样,控制台输出了 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
绑定到类名为 text
的 div
标签上~
首先咱们须要建立一个类,这里就叫作 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
,并从新定义 data
的 set
和 get
方法,保证咱们在 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-text
和 v-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
操做,分别更新 div
和 input
中的内容~
废话很少说,赶忙测试一下吧~
先初始化一下~
//建立vue实例
const app = new myVue({
el : '#app' ,
data : {
myText : 'hello world'
}
})
复制代码
接着,上图~
咱们顺利的实现了一个简单的双向绑定,棒棒哒 ~
如今,是否是已经对观察者模式有比较深入的理解了呢?其实,我这里说了这么多,只是起到了一个抛砖引玉的做用,重要的是设计思想,要学会将这种设计思想合理的应用到咱们实际的开发过程当中,可能过程会比较艰难,可是纸上得来终觉浅,绝知此事要躬行啊,你们加油~
哦,对了,今天 1024 啊 , 你们节日快乐哈~