自己来实现观察者模式
自己来实现观察者模式
今天写自己一个小demo时使用mvc的时候,想自己来实现观察者模式
也就是view层会观察model层数据的变化,相应渲染出改变后的数据
使用发布订阅模式
最开始想的办法是自己model拥有多个修改model.data的方法,如fetch(),patch(),delete(),post(),只需要每个方法里加一句
1 | event.emit('dataChanged',this.data) |
就能通知在监听的view层进行渲染了
不过有点不优雅,我需要在每个改变数据的方法后都加一句,很麻烦。
使用Object.defineProperty来自动化
之后想到了ES5提供的**Object.defineProperty**提供的get和set存取描述符,对这个属性的访问和读取会分别触发get和set函数, 也就是如果对data进行设置,就会触发set函数。
get()和set()又是什么?
我们平常取值或者赋值都是这样的
1 | data.val = 1 //现在val = 1 |
在这个过程中,val就是一个值,获得val和修改val总是相等的,修改成多少,之后就会得到多少。
不过js是门很奇怪的语言,有时候会发现获得的值和修改的值不一样,比如cookie
1 | document.cookie //xxx=111; yyy=222; |
看这里,赋值和取值并不相等,就像是自己调用的是方法一样
1 | document.cookie.get() //xxx=111; yyy=222 |
似乎是这样的感觉。
原理也就是这样,在js中我们可以把赋值这个过程当成函数调用
赋值调用set函数 ,取值使用get函数。
ES5就提供了Object.defineProperty来自己定义赋值和取值得行为Object.defineProperty接受三个参数,要定义的对象,要定义的键名,定义行为的对象
1 | var cookieList = [] |
这就是我们仿写的一个cookie,取值变成了调用get函数,赋值变成了调用set()函数
ES6有了定义对象的语法糖,也更简洁明了些
1 | let firstName = 'Barack' |
其实在这里设置get和set, 有点像java, java中一切皆对象,类中常常有私有变量不能被外部访问,就通过getName()和setName()暴露api
通过这种方式,就能对赋值和取值进行控制,比如赋值的时候检测值是否合法这些,js就直接省略了写函数这部分,直接就能把赋值、取值的行为当作函数调用。
不过这种也很误导,赋值和取值竟然会不相等,那为什么不直接告诉我这是一个方法?实际情况下不应该胡乱使用get和set
另外查了下,语法似乎更像c#
1 | Class example |
好了,回到正题,现在我们可以在赋值时,通知View层数据改变了
1 | let model = {} |
不过马上就发现Object.defineProperty 并不怎么好用
getset存取描述符会导致递归问题1
2
3
4
5
6
7
8
9let model = {}
Object.defineProperty(model,'data',{
set(value){
this.data = value
event.emit('dataChanged',this.data)
}
})
model.data //递归调用 页面会卡死解决办法很简单,不使用data就可以了,比如使用
this._data,或者使用闭包,利用外部环境的变量。如果
model.data的值不是原始类型,而是对象,那么对model.data对象的的更改不会触发set()1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18let model = {}
Object.defineProperty(model,'data',{
get(){
console.log('get')
return this._data
},
set(value){
this._data = value
console.log('set')
return this._data
}
})
model.data //get
model.data.push({a:1}) //get
let data = model.data //get
data.push({b:2}) //不会调用get set
data //不会调用get在实际情况中,往往
data都是一堆对象,存储的是引用,解决办法大概可以判断是否是对象,如果是,就递归的定义get和set,或者判断这是否是一个会更改data的函数,总而言之是件挺麻烦的事Object.defineProperty只能设置已知的属性,不能对未知的属性进行设置
这就导致上面递归解决方案行不通,因为在运行时定义的属性的key是未知的,以就无法设置get,set
使用Proxy来更优雅的实现
关于Proxy的说明可以点击这里,Proxy可以看作是Object.defineProperty()的完全升级版,可以拦截各种对对象的操作,其中就包括get和set,Vue3就是基于Proxy来实现响应式跟踪的。
1 | let array = [] |
Proxy翻译为代理,其实挺贴切的,从上面的例子中可以看出,Proxy并不会修改原始对象的行为,而是会生成一个代理对象,用于拦截各种操作。
同时浏览器还提供了Reflect用来更好的扩展默认行为,阮一峰在ES6文中是这样说的:
Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。
这个概念类似于子类重写父类的行为一样,可以让我们方便的在原来逻辑基础上添加一扩展行为。
最后贴下大概的最终代码
1 | //能够检测所有变动的核心,递归检测所有赋值为对象的情况,并代理这个对象 |
这只是一个小demo,能够实现data内任何数据的改变都能检测到,可以复制到浏览器控制台验证,可能还存在bug,实际情况下应该还会涉及更复杂的错误处理还有性能方面的问题