Object的变化侦探
ES6之前主要通过Object.defineProperty方法,ES6之后通过Proxy来实现对数据的侦查.
基于Object.defineProperty:对象的变化是靠setter来追踪的,一旦数据发生了变化,便会触发setter.例如下面的例子
Object.defineProperty(data ,key ,{
value: 'tangytin'
enumerable: true,
configurable: true,
get: function() {},
// 当data.key发生变化,自动触发set,
set: function(newVal) {},
})
当data.key的值发生变化的时候,会触发set, 可以得到变化后的值newVal。但是想要实现追踪数据变化,当然只是上面的代码远远不够,我们要考虑以下因素
- 有哪些地方用到data.key,
- 如何收集所有用到data.key的地方,
- 如何触发每一处的data.key都发生变化
仔细想想以上两个因素,其实和我们之前提到的发布-订阅者模式很相像:但这种模式叫做观察者模式,那么观察者模式和发布-订阅者模式之间的区别,请看以下链接:developer.aliyun.com/article/499…
- 收集data.key(订阅)
- 触发data.key,获取变化后的值做一些事情(发布)
现在我们将追踪依赖的defineProperty封装起来,放入defineReactive函数中,并且给出一个数组dep来收集当前的data.key所有用到的地方(依赖),假设当前的依赖是一个函数(window.target),并且在data.key发生变化的时候触发所有的依赖项
function defineReactive(data, key, val) {
// 依赖收集数组
const dep = []
Object.defineProperty(data ,key ,{
enumerable: true,
configurable: true,
get: function() {
dep.push(window.target)
return val
},
// 当data.key发生变化,自动触发set,
set: function(newVal) {
for(let i = 0; i < dep.length; i++){
dep[i](newVal,val)
}
if(newVal!==val) return newVal
},
})
}
这样一个简单的依赖收集就完成了,但是考虑到代码的耦合性,所以决定将收集依赖和依赖都单独放出来封装成一个类,我们用Dep来收集依赖,用Watcher来作为依赖的角色。
class Dep {
constructor() {
this.subs = []
}
addSub(sub) {
this.subs.push(sub)
}
removeSub(sub) {
if(this.subs.length) {
const index = this.subs.indexOf(sub)
if(index > -1)
this.subs.splice(index,1)
}
}
depend(sub) {
if(window.target) {
this.addSub(sub)
}
}
notify() {
const subs = this.subs.slice()
for(let i = 0; i < subs.length; i++) {
subs[i].update()
}
}
}
//同时对defineReactive进行相应的修改
function defineReactive(data, key, val) {
// 依赖收集数组
const dep = new Dep()
Object.defineProperty(data ,key ,{
enumerable: true,
configurable: true,
get: function() {
dep.depend() //1
return val
},
// 当data.key发生变化,自动触发set,
set: function(newVal) {
dep.notify() //2
if(newVal!==val) return newVal
},
})
}
那么Watcher依赖是怎么来的呢,我所理解的依赖是当使用data.key的时候,就相当于订阅了
vm.$watch(key,cb)即:
vm.$watch('data.key', function(newVal, oldVal){
//回掉做响应的处理
})
那么将以上内容抽象成类
class Watcher(){
constructor(vm, expOrFn, cb){
this.vm = vm
// parsePath可以读取字符串的keyPath
this.getter = parsePath(expOrFn)
this.cb = cb
this.value = this.get()
}
get(){
// 将自己(依赖)设置给window.target
window.target = this
// 触发getter,进行收集依赖
let value = this.getter.call(this.vm, this.vm)
//清空window.target
window.target = undefined
return value
}
// 触发依赖是,更新(调用回掉函数)
update() {
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}
现在就完美的实现了侦测数据中的某一个属性,如果我们想要侦测数据中的所有属性,还需要进行递归。将其封装进行一个Observer类
class Observer {
constructor(value) {
// 侦测的数据
this.value = value
//数组的侦测另作处理,下一章节介绍
if(!Array.isArray(value)) {
this.walk(value)
}
}
walk(obj) {
const keys = Object.keys(obj)
for(let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
}
//同时对defineReactive进行相应的修改
function defineReactive(data, key, val) {
// 依赖收集数组
const dep = new Dep()
// 递归
if(typeof val === 'object') {
new Observer(val)
}
// 侦测单个
Object.defineProperty(data ,key ,{
enumerable: true,
configurable: true,
get: function() {
dep.depend()
return val
},
// 当data.key发生变化,自动触发set,
set: function(newVal) {
dep.notify()
if(newVal!==val) return newVal
},
})
}
总结
Object通过使用Object.defineProperty将属性转换成getter/setter来追踪变换,在getter中进行依赖收集,setter中触发通知收集的依赖发生变化