Object的变化侦测
像Vue官网上面说的,vue是通过Object.defineProperty
来侦测对象属性值的变化。
function defineReactive (obj, key, val) {
let dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get () {
return val
},
set (newVal) {
if (val === newVal) return
val = newVal
}
})
}
函数 defineReactive 是对 Object.defineProperty 的封装,作用是定义一个响应式的数据。
不过如果只是这样是没有什么用的,真正有用的是收集依赖。在getter中收集依赖,在setter触发依赖。
Dep (收集依赖)
// 还有几个方法没写,比如怎么移除依赖。
class Dep {
constructor () {
// 依赖数组
this.subs = []
}
addSub (sub) {
this.subs.push(sub)
}
depend (target) {
if (Dep.target) {
// 这时的Dep.target是Watcher实例
Dep.target.addDep(this)
}
}
notity () {
this.subs.forEach(val => {
val.update()
})
}
Dep.target = null
}
Watcher (依赖)
// 本来在Watcher中也要记录Dep,但是偷懒没写了,记录了Dep后可以通知收集了Watcher的Dep移除依赖。
class Watcher {
constructor (vm, expOrFn, cb) {
// vm: vue实例
// expOrFn: 字符串或函数
// cb: callback回调函数
this.vm = vm
this.cb = cb
// 执行this.getter就可以读取expOrFn的数据,就会收集依赖
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// parsePath是读取字符串keypath的函数,具体的可以去浏览Vue的源码
this.getter = parsePath(expOrFn)
}
this.value = this.get()
}
get () {
Dep.target = this
// 在这里执行this.getter
let value = this.getter(this.vm, this.vm)
Dep.target = null
return value
}
addDep (dep) {
dep.addSub(this)
}
// 更新依赖
update () {
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}
接下来再改一下刚开始定义的 defineReactive 函数
function defineReactive (obj, key, val) {
let dep = new Dep() // 闭包
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get () {
// 触发getter时,收集依赖
dep.addDep()
return val
},
set (newVal) {
if (val === newVal) return
val = newVal
// 触发setter时,触发Dep的notify,便利依赖
dep.notity()
}
})
}
这个时候已经可以侦测数据的单独一个属性,最后再封装一下:
class Observer {
constructor (value) {
this.value = value
// 侦测数据的变化和侦测对象的变化是有区别的
if (!Array.isArray(value)) {
this.walk(value)
}
}
walk (value) {
const keys = Object.keys(value)
keys.forEach(key => {
this.defineReactive(value, key, value[key])
})
}
}
最后总结一下:
实例化 Watcher 时通过 get 方法把 Dep.target 赋值为当前的 Wathcer 实例,并把 Watcher 实例添加在 Dep 中,当设置数据时,触发 defineReactive 的 set 运行 Dep.notify() 遍历 Dep 中收集的依赖 Watcher 实例,然后触发 Watcher 实例的 update 方法。
Array的变化侦测
Object 可以通过 getter/setter 来侦测变化,但是数组是通过方法来变化,比如 push 。这样就不能和对象一样,只能通过拦截器来实现侦测变化。
定义一个拦截器来覆盖 Array.prototype,每当使用数组原型上面的方法操作数组的时候,实际上执行的是拦截器上面的方法,然后再拦截器里面使用 Array 的原型方法。
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
// 缓存原始方法
const original = arrayProto[method]
Object.defineProperty(arrayMethods, method, {
enumerable: false,
configurable: true,
writable: true,
value: function mutator (...args) {
return original.apply(this, args)
}
})
然后就要覆盖 Array 的原型:
// 看是否支持__proto__, 如果不支持__proto__,则直接把拦截器的方法直接挂载到value上。
const hasProto = "__proto__" in {}
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
class Observer {
constructor (value) {
this.value = value
if (!Array.isArray(value)) {
this.walk(value)
} else {
const augment = hasProto ? protoAugment : copyAugment
augment(value, arrayMethods, arraykeys)
}
}
walk (value) {
const keys = Object.keys(value)
keys.forEach(key => {
this.defineReactive(value, key, value[key])
})
}
}
function protoAugment (target, src: Object) {
target.__proto__ = src
}
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
Array 也是在 getter 中收集依赖,不过依赖存的地方有了变化。Vue.js 把依赖存在 Observer 中:
class Observer {
constructor (value) {
this.value = value
this.dep = new Dep // 新增Dep
if (!Array.isArray(value)) {
this.walk(value)
} else {
const augment = hasProto ? protoAugment : copyAugment
augment(value, arrayMethods, arraykeys)
}
}
walk (value) {
const keys = Object.keys(value)
keys.forEach(key => {
this.defineReactive(value, key, value[key])
})
}
}
至于为什么把 Dep 存在 Observer 是因为必须在 getter 和 拦截器中都能访问到。
function defineReactive (data, key, val) {
let childOb = observer(val) // 新增
let dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get () {
dep.addDep()
if (childOb) {
// 在这里收集数组依赖
childOb.dep.depend()
}
return val
},
set (newVal) {
if (val === newVal) return
val = newVal
dep.notity()
}
})
}
// 如果value已经是响应式数据,即有了__ob__属性,则直接返回已经创建的Observer实例
// 如果不是响应式数据,则创建一个Observer实例
function observer (value, asRootData) {
if (!isObject(value)) {
return
}
let ob
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observe) {
ob = value.__ob__
} else {
ob = new Observer(value)
}
return ob
}
因为拦截器是对 Array 原型的封装,所以可以在拦截器中访问到this(当前正在被操作的数组),
dep保存在 Observer 实例中,所以需要在this上访问到 Observer 实例:
function def (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configerable: true
})
}
class Observer {
constructor (value) {
this.value = value
this.dep = new Dep
// 把value上新增一个不可枚举的属性__ob__,值为当前的Observer实例
// 这样就可以通过数组的__ob__属性拿到Observer实例,然后就可以拿到Observer的depp
// __ob__不止是为了拿到Observer实例,还可以标记是否是响应式数据
def(value, '__ob__', this) // 新增
if (!Array.isArray(value)) {
this.walk(value)
} else {
const augment = hasProto ? protoAugment : copyAugment
augment(value, arrayMethods, arraykeys)
}
}
...
}
在拦截器中:
methodsToPatch.forEach(function (method) {
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__ // 新增
ob.dep.notify() // 新增 向依赖发送信息
return resullt
})
})
到这里还只是侦测了数组的变化,还要侦测数组元素的变化:
class Observer {
constructor (value) {
this.value = value
this.dep = new Dep
def(value, '__ob__', this)
if (!Array.isArray(value)) {
this.walk(value)
} else {
const augment = hasProto ? protoAugment : copyAugment
augment(value, arrayMethods, arraykeys)
// 侦测数组中的每一项
this.observeArray(value) // 新增
}
}
observeArray (items) {
items.forEach(item => {
observe(item)
})
}
...
}
然后还要侦测数组中的新增元素的变化:
methodsToPatch.forEach(function (method) {
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
// 新增开始
let inserted
switch (method) {
case 'push'
case 'unshift'
inserted = args
breaak
case 'splice'
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// 新增结束
ob.dep.notify()
return resullt
})
})
总结一下:
Array 追踪变化的方式和 Object 不一样,是通过拦截器去覆盖数组原型的方法来追踪变化。
为了不污染全局的 Array.prototype ,所以只针对那些需要侦测变化的数组,对于不支持 __proto__
的浏览器则直接把拦截器布置到数组本身上。
在 Observer 中,对每个侦测了变化的数据都加了 __ob__
属性,并且把this(Observer实例)
保存在__ob__
上,主要有两个作用:
- 标记数据是否被侦测了
- 可以通过数据拿到
__ob__
,进一步拿到 Observer 实例。
所以把数组的依赖存放在 Observer 中,当拦截到数组发生变化时,向依赖发送通知。
最后还要通过observeArray
侦测数组子元素和数组新增元素的变化。