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
侦测数组子元素和数组新增元素的变化。