前言
这章节承上章节漏掉的数组观测、新增属性观测
正文
观测数组
我们回到Observer
的这段代码
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
上章节我们走的是else
,这次我们走if
,即value
是数组的情况
我们先判断当前环境是否支持__proto__
,看情况分别使用protoAugment、copyAugment
,将其赋值给augment
augment(value, arrayMethods, arrayKeys)
我们先搞清楚arrayMethods、arrayKeys
分别是什么
export const arrayMethods = Object.create(arrayProto)
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
溯源可知arrayMethods
就是数组的原型对象,所以我们再看protoAugment、copyAugment
function protoAugment(target, src: Object, keys: any) {
target.__proto__ = src
}
function copyAugment(target: Object, src: Object, keys: Array) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
可见protoAugment
就是将arrayMEthods
赋值给value.__proto__
,也就是将处理过的数组原型上的方法赋值给数组原型,也就是劫持下数组原型对象,这样子我们就可以在调用[].splice
之类的方法时在不破坏原生的操作之后加上自己的一些操作
copyAugment
就是在数组不支持__proto__
时,那我们就需要遍历arrayKeys
,然后使用def
(Object.defineProperty
)逐项设置,这样子也可以达到类似的效果
这样子就能做到在调用.splice
之类的方法时可以执行注入到原型上的逻辑
这只是修改了数组对象的原型对象指向,将其指向修改过的
arrayMethods
。也就是并不是所有的数组对象都会被劫持,只有被观测的数组对象才会被劫持
最后this.observeArray(value)
observeArray(items: Array) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
就是遍历数组,然后逐项观测即可
arrayMethods
接下来看看arrayMethods
对Array.prototype
做了什么改动
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)
ob.dep.notify()
return result
})
})
首先我们取到数组原型对象,然后通过Object.create
复制一份副本。因为劫持必然会对原本的做出改动,使用副本的话不会影响原本的
然后定义methodsToPatch
变量存储会对数组做出改动的方法,因为若不会对数组做出改动就没有什么劫持的价值
以上是准备工作,接下来遍历methodsToPatch
。首先取得该项原方法赋值给original
,然后使用def
覆盖在原型上的此方法,既然是劫持,就不好做些影响原本结果的情况,就比如push
的结果,劫持完了的push
也该和原本的一致
const result = original.apply(this, args)
// ...省略
return result
这个就是调用原方法得到操作结果,最后返回
接下来看我们注入的逻辑
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)
ob.dep.notify()
首先取得这个数组对象对应的__ob__
赋值给ob
,然后我们试想下,这么些个方法里有几个可是会增加新元素的,新的值自然也是需要观测的,所以我们得拿到这部分新值。对于push、unshift
,args
就是新增的元素,splice
可新增也可删除,新增的话就是参数的第三项,所以取args.slice(2)
。然后简单了,判断inserted
存在的话就调用ob.observeArray(inserted)
,最后调用ob.dep.notify()
,触发该数组对象上收集到的依赖
观测数组和观测对象为何要区分
我们可以看到数组和纯对象观测是不一样的,纯对象的话每个键值都Object.defineProperty
处理过,而数组的话索引是没有被处理过的,这也就导致了数组的索引是非响应式的
这个在官网有提到
其实这里很多人看到会有 误区,也就是是不是
Object.defineProperty
监测不到索引变动什么的,其实不是。看这个 issue8562
也就是其实完全可以当做纯对象处理,不过终究是
性能代价和获得的用户体验收益不成正比
Vue.set
上章节我们简单说了下新增属性原理,也就是Vue.set
,即:
- 将新属性值转为响应式
- 触发新属性宿主对象收集到的依赖(
__ob__
)
新增我们根据这个思路来看看Vue.set
源码
export function set(target: Array | Object, key: any, val: any): any {
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
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
}
if (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
首先判断下该宿主对象情况,不能是undefined、null、原始类型
然后判断下若是数组,而且key
是有效地索引,那么直接用splice
就行了
接下来这段有点门道,所以深究下
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
这个if
有俩条件,即key
在target
或者其原型链上且不能在Object.prototype
上,那么就直接修改值就行了。其实原本并非如此,详情看这issues/6845。原本仅仅是if (hasOwn(target, key))
class Model {
constructor() {
this.foo = '123'
this._bar = null
}
get bar() {
return this._bar;
}
set bar(newvalue) {
this._bar = newvalue;
}
}
data = new Model()
试想若是target、key
分别是data、'bar'
那么hasOwn(data, 'bar') === false
、'bar' in data && !('bar' in Object.prototype) === true
可见前者会当做新增属性,后者直接当做已有属性,直接修改即可,即触发set bar
最后代码到这了就必然是新增属性
首先就是简单的取下ob
对象,然后就是揭示一个规矩:
- 不能给
Vue
实例设置新属性
这个就是可能出现覆盖情况 - 不能给根
data
设置新属性
这个有点讲究,其实呢是可以的,如demo4。它为什么不可以呢,我们知道initData
里有对data
实现了代理访问即proxy(vm, '_data', key)
。也就是vm.a === vm._data.a
。我们新增的自然也就没有这层代理,那么根数据新增属性自然也就不能vm.nVal
这样子访问了。所以如例子所示,自行做了这个代理就可以啦
然后要是ob
不存在的话就说明这个target
非响应式,简单设置即可
最后就是defineReactive
转化成响应式,并且ob.dep.notify()
触发依赖更新
Vue.del
export function del(target: Array | Object, key: any) {
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
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
}
if (!hasOwn(target, key)) {
return
}
delete target[key]
if (!ob) {
return
}
ob.dep.notify()
}
首先就是和Vue.set
一样的判定以及数组情况下调用劫持过的数组方法处理还有Vue实例对象以及根data
不能操作的限定
然后就是if (!hasOwn(target, key)) {
,这个就是判定该对象上有没有该属性,没有的话自然就return
。这里为什么不用和Vue.set
里一样呢,这是因为delete操作只会在自身的属性上起作用,要删除原型链上的属性就得传入那个原型对象
最后就是删除该属性,判断下ob
不在的话就return
,因为不是响应式的自然不用触发更新,是的话就ob.dep.notify()
触发依赖更新