Vue响应式原理之Observer
之前简单介绍了Dep和Watcher类的代码和作用,现在来介绍一下Observer类和set/get。在Vue实例后再添加响应式数据时需要借助Vue.set/vm.$set
方法,这两个方法内部实际上调用了set方法。而Observer所做的就是将修改反映到视图中。
Observer
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
Observer
有三个属性。value
是响应式数据的值;dep
是Dep实例,这个Dep实例用于Vue.set/vm.$set
中通知依赖更新;vmCount
表示把这个数据当成根data对象的实例数量,大于0时是实例化传入的根data对象。
构造函数接受一个值,表示要观察的值,这样,在Observer实例中引用了响应式数据,并将响应式数据的__ob__
属性指向自身。如果被观察值是除数组以外的类型,会调用walk
方法,令每个属性都是响应式。对于基本类型的值,Object.keys
会返回一个空数组,所以在walk
内,defineReactive
只在对象的属性上执行。如果是被观察值是数组,那么会在每个元素上调用工厂函数observe
,使其响应式。
对于数组,响应式的实现稍有不同。回顾一下在教程数组更新检测里的说明,变异方法会触发视图更新。其具体实现就在这里。arrayMethods
是一个对象,保存了Vue重写的数组方法,具体重写方式下面再说,现在只需知道这些重写的数组方法除了保持原数组方法的功能外,还能通知依赖数据已更新。augment
的用途是令value
能够调用在arrayMethods
中的方法,实现的方式有两种。第一种是通过原型链实现,在value.__proto__
添加这些方法,优先选择这种实现。部分浏览器不支持__proto__
,则直接在value
上添加这些方法。
最后执行observeArray
方法,遍历value
,在每个元素上执行observe
方法。
数组变异方法的实现
执行变异方法会触发视图功能,所以变异方法要实现的功能,除了包括原来数组方法的功能外,还要有通知依赖数据更新的功能。代码保存在/src/core/observer/array.js
。
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
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
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
模块内,使用arrayProto
保存数组原型,arrayMethods
的原型是arrayProto
,用来保存变异后的方法,methodsToPatch
是保存变异方法名的数组。
遍历methodsToPatch
,根据方法名来获取在arrayProto
上的数组变异方法,然后在arrayMethods
实现同名方法。
在该同名方法内,首先执行缓存的数组方法original
,执行上下文是this
,这些方法最终会添加到响应式数组或其原型上,所以被调用时this
是数组本身。ob
指向this.__ob__
,使用inserted
指向被插入的元素,调用ob.observeArray
观察新增的数组元素。最后执行ob.dep.notify()
,通知依赖更新。
observe
工厂函数,获取value上__ob__
属性指向的Observer实例,如果需要该属性且未定义时,根据数据创建一个Observer实例,在实例化时会在value上添加__ob__
属性。参数二表示传入的value
是否是根data对象。只有根数据对象的__ob__.vmCount
大于0。
isObject
判断value
是不是Object类型,实现如obj !== null && typeof obj === 'object'
。
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
此处可以看出,value
与Observer实例ob
之间是双向引用。value.__ob__
指向ob
,ob.value
指向value
。
Vue.set
在Vue实例化以后,如果想为其添加新的响应式属性,对于对象,直接使用字面量赋值是没有效果的。由响应式数据的实现可以想到,这种直接赋值的方式,并没有为该属性自定义getter/setter,在获取属性时不会收集依赖,在更新属性时不会触发更新。如果想要为已存在的响应式数据添加新属性,可以使用Vue.set/vm.$set
方法,但要注意,不能在data上添加新属性。
Vue.set/vm.$set
内部都是在/src/code/observer/index.js
定义的set
的函数。
set
函数接受三个参数,参数一target
表示要新增属性的对象,参数二key
表示新增的属性名或索引,参数三val
表示新增属性的初始值。
export function set (target: Array<any> | Object, key: any, val: any): any {
if (process.env.NODE_ENV !== 'production' &&
!Array.isArray(target) &&
!isObject(target)
) {
warn(`Cannot set reactive property on non-object/array value: ${target}`)
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
// 不存在ob 说明不是响应式数据
if (!ob) {
target[key] = val
return val
}
// 为target添加新属性
defineReactive(ob.value, key, val)
// ob.dep实际是target.__ob__.dep
ob.dep.notify()
return val
}
函数内部首先判断target
类型,非数组或非对象的目标数据是无法添加响应式数据的。
如果是数组,且key
是有效的数组索引,更新数组长度,然后调用变异方法splice
,更新对应的值并触发视图更新。如果是对象,且属性key
在target
的原型链上且不在Object.prototype
上(即不是Object原型上定义的属性或方法),直接在target
上添加或更新key
。
令ob
指向target.__ob__
,如果target
是Vue实例或是根data对象(ob.vmCount > 0
),则无法新增数据,直接返回。
接着处理能为target
添加属性的情况。不存在ob时,说明不是响应式数据,直接更新target
。否则,执行defineReactive
函数为ob.value
新增响应式属性,ob.value
实际指向target
,添加之后调用ob.dep.notify()
通知观察者重新求值,ob是Observer实例。
总结一下,set的内部逻辑:
当target
是数组时,更新长度,调用变异方法splice
插入新元素即可。
当target
是对象时:
-
key
在除Object.prototype
外的原型链上时,直接赋值 -
key
在原型链上搜索不到时,需要新增属性。如果target
无__ob__
属性,说明不是响应式数据,直接赋值。否则调用defineReactive(ob.value, key, val)
观察新数据,同时触发依赖。
Vue.delete
删除对象的属性。如果对象是响应式的,确保删除能触发更新视图。
Vue.delete
实际指向del
。del
接受两个参数,参数一target
表示要删除属性的对象,参数二key
表示要删除的属性名。
如果target
是数组且key
对于的索引在target
中存在,使用变异方法splice
方法直接删除。
如果target
是Vue实例或是根data对象则返回,不允许在其上删除属性。key
不是实例自身属性时也返回,不允许删除。如果是自身属性则使用delete
删除,接着判断是否有__ob__
属性,如果有,说明是响应式数据,执行__ob__.dep.notify
通知视图更新。
export function del (target: Array<any> | Object, key: any) {
if (process.env.NODE_ENV !== 'production' &&
!Array.isArray(target) &&
!isObject(target)
) {
warn(`Cannot delete reactive property on non-object/array value: ${target}`)
}
// 数组 直接删除元素
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1)
return
}
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid deleting properties on a Vue instance or its root $data ' +
'- just set it to null.'
)
return
}
// 属性不在target上
if (!hasOwn(target, key)) {
return
}
delete target[key]
// 不是响应式数据
if (!ob) {
return
}
ob.dep.notify()
}
小结
关于Observer类和set/get的源码已经做了简单的分析,细心的读者可能会有一个问题:target.__ob__.dep
是什么时候收集依赖的。答案就在defineReactive
的源码中,其收集操作同样在响应式数据的getter中执行。
至于defineReactive
的源码解析,在后面的文章再做分析。