自己来实现观察者模式
自己来实现观察者模式
今天写自己一个小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
并不怎么好用
get
set
存取描述符会导致递归问题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,实际情况下应该还会涉及更复杂的错误处理还有性能方面的问题