由来
最近在看“深入浅出vuejs”,第一篇变化侦测,想把自己的理解总结一下。
Object的变化侦测
总结一下我看了后的理解
- 将数据变成可响应式的,即将数据变成可监听的。通过
Observer
类来实现 - 依赖是什么?就是这个数据在哪里用到了,相当于
this
当前的上下文;所以当数据变化时,我们可以通知他,触发update
,从而触发渲染 - 那么这个依赖,谁来收集存起来。通过
Dep
类来实现
先看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++) {
definedReactive(obj, keys[i], obj[keys[i]])
}
}
}
function definedReactive(data, key, value) {
if(typeof val === 'object') {
new Observer(value)
}
let dep = new Dep()
Object.defineProperty(data, key, {
enumberable: true,
configurable: true,
get: function () {
dep.depend()
return value
},
set: function (newVal) {
if(value === newVal) { //这边最好是value === newVal || (value !== value && newVal !== newVal)
return
}
value = newVal //这边新的newVal如果是引用类型也应该进行进行new Observer()
dep.notify()
}
})
}
很容易看懂
- 将vue中的
data
对象进行遍历设置其属性描述对象 -
get
的设置就是为了在数据被访问时,将依赖dep.depend()
进去,至于做了什么看详细看Dep类 -
set
的设置则是为了判断新值和旧值是否一样(注意NaN),若不一样,则执行dep.notify()
,通知相应依赖进行更新变化
Dep类
class Dep {
constructor () {
this.subs = [] //存放依赖
}
addSub () {
this.subs.push(sub)
},
remove () {
remove(this.subs, sub)
},
depend () {
if(window.target) {
this.addSub(window.target) //window.target 是this,watcher的上下文
}
},
notify () {
const subs = this.subs.slice()
for(let i = 0, l = subs.length; i < l; i ++) {
subs[i].update() //update这个方法来自watcher实例对象的方法
}
}
}
function remove(arr, item) {
if(arr.length) {
const index = arr.indexOf(item)
if(index > -1) {
return arr.splice(index, 1)
}
}
}
分析一下
- 主要就是对
dep
实例对象的增删改查的操作 -
window.target
这个依赖怎么来,就看watcher
实例对象了
Watcher类
初版:
class Watcher {
constructor (vm, expOrFn, cb) {
this.vm = vm
this.getter = parsePath(expOrFn)
this.cb = cb
this.value = this.get()
}
get() {
window.target = this
let value = this.getter.call(this.vm, this.vm)
window.target = undefined
return value
}
update() {
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}
分析
- 怎么触发?可以利用
vm.$watch('data.a', function (newValue, oldValue) {
//执行相关操作
})
-
parsePath(expOrFn)
做了什么?从下面代码中可以看出作用就是返回一个函数,这个函数用来读取value
值
const bailRE = /[^\w.$]/ //
function parsePath(path) {
if(bailRE.test(path) {
return //当path路径中有一个字符不满足正则要求就直接return
}
return function () {
const arr = path.split('.')
let data = this
for(let i = 0, l = arr.length; i < l; i ++) {
let data = data.arr[i]
}
return data
}
}
- 在
new Watcher
时会执行this.value
,从而执行this.get()
,所以这时的window.target
是当前watcher
实例对象this
;接着执行this.getter.call(this.vm, this.vm)
,触发属性描述对象的get
方法,进行dep.depend()
,最后将其window.target = undefined
-
update
的方法是在数据改变后触发,但这边有个问题就是会重复添加依赖
上面版本中比较明显的问题
- 依赖被重复添加
- 只能对已有
key
进行监听 - 删除
key-value
不会被监听 - 对数组对象,并没有添加监听
- 对于数据变化时,并没有对新数据判断是否需要进行
Observer
Array的侦测
怎么实现在数组发生变化时来触发dep.notify()
,以及如何收集数组的依赖
- 通过
push, pop, shift, unshift, splice, sort, reverse
这几个方法的封装来触发dep.notify()
- 怎么的封装?分两种;第一种对于支持
_proto_
属性的,直接改写原型链的这些方法;第二种对于不支持的,直接在实例对象上添加改变后的7个方法
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto) //新建对象,继承Array的原型链
class Observer {
constructor (value) {
this.value = value
this.dep = new Dep() //在Observer中添加dep属性为了记录数组的依赖
def(value, "_ob_", this) //在当前value上新增`_ob_`属性,其值为this,当前observer实例对象
if(Array.isArray(value) {
const augment = hasProto ? protoAugment : copyAugment
augment(value, arrayMethods, arrayKeys)
this.observerArray(value) //将数组内元素也进行Observer
}else {
this.walk(value)
}
}
//新增
observerArray (items) {
for(let i = 0, l = items.length; i < l; i ++) {
observe(items[i])
}
}
}
//作用就是为obj,添加key值为val
function def(obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
function observe(value, asRootData) {
if(!isObject(value)) {
return
}
let ob
//判断value是否已经是Observer实例对象,避免重复执行Observer
if(hasOwn(value, "_ob_") && value._ob_ instanceof Observer) {
ob = value._ob_
} else {
ob = new Observer(value)
}
return ob
}
function definedReactive(data, key, value) {
let childOb = observe(value) //修改
let dep = new Dep()
Object.defineProperty(data, key, {
enumberable: true,
configurable: true,
get: function () {
dep.depend()
if(childOb) { //新增
childOb.dep.depend()
}
return value
},
set: function (newVal) {
if(value === newVal) { //这边最好是value === newVal || (value !== value && newVal !== newVal)
return
}
value = newVal //这边新的newVal如果是引用类型也应该进行进行new Observer()
dep.notify()
}
})
}
//触发数组拦截
;[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
].forEach(function (method) {
const original = arrayProto[method]
def(arrayMethods, method, function mutator() {
const result = original.apply(this, args)
const ob = this._ob_ //this就是数据value
let inserted
//对于新增变化的元素页进行observerArray()
switch (method) { //因为这几个是有参数的
case 'push':
case 'unshift': //因为push和unshift都是一样的取args,所以push不需要加break了
inserted = args
break
case 'splice': //新增变化元素是从索引2开始的
inserted = args.slice(2)
break
}
ob.dep.notify() //通知依赖执行update
return result
})
}
分析,已data = { a: [1, 2, 3] }
为例
- 首先对
data
对象进行Observer
,将执行this.walk(data)
- 接着执行
let childOb = observe(val)
,发现value
是一个数组对象,进行Observer
,主要进行是augment(value, arrayMethods, arrayKeys)
,将7个方法进行拦截,接着遍历内部元素是否有引用数据类型,有继续Observer
,最后返回Observer
实例对象ob
- 重点是
get
方法,当数据data被访问时,首先执行dep.depend()
这里将依赖添加到data
的dep
中;接着因为childOb
为true
所以执行childOb.dep.depend()
,这里是将依赖加入到observer
实例对象的dep
中,为什么,这个dep
是给数组发生变化时执行this._ob_.dep.notify()
,这个this就是value
对象,因为def(value, "_ob_", this)
,所以可以执行dep.notify()
这种数组变化侦测存在的问题
- 对于进行
this.list.length = 0
进行清空时,不会触发它的依赖更新,也就不会触发视图的渲染更新 - 对于
this.list[0] = 2
,这种通过索引来改变元素值时页一样不会触发更新 - 所以我们尽量避免通过这种方式来改变数据