这篇文章将介绍Vue2的响应式数据如何处理数组的情况以及介绍Vue2的响应式数组有哪些不足
参考《深入浅出Vue.js》,Vue2源码
1. 如何拦截数组
Object.defineProperty
这个API无法拦截数组的原型方法
,也无法拦截数组的索引赋值
和length属性
的修改
js
let arr = ['1']
Object.defineProperty(arr, 1, {
value: '1',
configurable: true,
writable: true,
enumerable: true,
getter: function (val) {
// 不会触发
console.log(val, 'val');
return val;
},
setter: function (val) {
// 不会触发
console.log(val, 'val');
}
})
arr[1] = 3
arr.push(2)
arr.length = 0
我们能做的是增加一个拦截器,真正执行方法时使用的是拦截器中的方法,在拦截器里面使用数组的原型方法
js
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
[
'push',
'pop',
'unshift',
'shift',
'splice',
'sort',
'reverse'
].forEach(method =>{
const original = arrayProto[method]
Object.defineProperty(arrayMethods, method, {
// 最终执行的是这个方法
value: function mutator (...args) {
// 执行original,this指向这里
return original.apply(this, args)
},
enumerable: false,
writable: true,
configurable: true
})
})
如上代码中,以Array.prototype
为原型,创建了一个新的对象arrayMethods
;使用Object.defineProperty
对每个方法如push pop
等进行拦截,当调用数组方法时执行的实际上是value对应的mutator
方法,mutator里面original.apply
是使用原生
的Array的原型方法操作数组
2. 拦截器只修改响应式数据
如下,我们把arrayMethods仅仅赋值给响应式数据的__proto__
属性,这样就不会污染全局数组的原型,只有响应式数据的原型受到影响
diff
class Observer {
constructor (value) {
this.value = value
// 如果是数组
if (Array.isArray(value)) {
+ value.__proto__ = arrayMethods
}
// 如果是对象
else {
this.walk(value)
}
}
walk (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
}
判断是否有__proto__属性
js
// 是否有__proto__属性
const hasProto = '__proto__' in {}
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
如果有__proto__属性,调用protoAugment
直接赋值原型;如果没有,则调用copyAugment
把拦截器中的方法挂载到value上
js
class Observer {
constructor (value) {
debugger
this.value = value
// 如果是数组
if (Array.isArray(value)) {
// 覆盖数组的原型
// value.__proto__ = arrayMethods
// 修改为
const augment = hasProto ? protoAugment : copyAugment
augment(value, arrayMethods, arrayKeys)
}
// 如果是对象
else {
this.walk(value)
}
}
walk (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
observeArray (items) {
// 数组所有子数据转化为响应式
debugger
for (let i = 0, l = items.length; i < l; i++) {
debugger
observe(items[i])
}
}
}
// def工具函数
function def (obj, key, val, enumerable) {
// 添加__ob__属性用
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
configurable: true,
writable: true
})
}
function protoAugment (target, src, keys) {
debugger
target.__proto__ = src
}
// 如果不支持__proto__,直接将arrayMethods上面的方法挂载到到被侦测的数组上
function copyAugment (target, src, keys) {
for (let i = 0, l = keys.length; i < keys.length, i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
}
3. 收集依赖
Vue2
把数组的依赖保存在Observer
类上,所以要在Observer类上绑定一个dep数组
,同时记录__ob__
属性,他的值就是当前Observer实例
,
这样以后响应式数据
可以直接通过this.__ob__
拿到Observer实例,进而拿到dep依赖数组
,往里面存数据
diff
class Observer {
constructor (value) {
this.value = value
+ this.dep = new Dep()
+ def(value, '__ob__', this)
// 如果是数组
if (Array.isArray(value)) {
// 覆盖数组的原型
// value.__proto__ = arrayMethods
// 这样也可以实现
// Object.setPrototypeOf(value, arrayMethods)
// 修改为
debugger
const augment = hasProto ? protoAugment : copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
}
// 如果是对象
else {
this.walk(value)
}
}
......
}
如下代码,在defineReactive函数内调用observe
函数,该函数会返回这个val
对应的Observer实例
。在下面会进一步介绍该方法。
封装observe
方法,判断如果数据身上挂载__ob__
属性,表示已经创建过Observer
实例了直接返回;否则针对该数据创建一个新的Observer
类,返回ob也就是Observer
实例。这是__ob__
的另一个作用
在getter中判断如果有childOb
,则执行childOb.dep.depend()
,是针对数组
进行的依赖收集
diff
function defineReactive (data, key, val) {
+ let childOb = observe(val) // 数组的Observer实例
let dep = new Dep()
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
// 为对象收集依赖
dep.depend()
// 为数组收集依赖
+ if (childOb) {
+ childOb.dep.depend()
+ }
+ return val
},
set: function (newVal) {
if (val === newVal) {
return
}
val = newVal
// 通知依赖
dep.notify()
console.log(val, 'val');
}
})
}
function observe (value) {
debugger
// 不是对象
if (!isObject(value)) return
let ob;
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
// 已经存在响应式属性ob => 数组已经是响应式数据了
+ ob = value.__ob__
} else {
// 数组此时还不是响应式数据
+ ob = new Observer(value)
}
return ob
}
对应源码
diff
/src/core/observer/index.ts
export function defineReactive(
obj: object,
key: string,
val?: any,
customSetter?: Function | null,
shallow?: boolean,
mock?: boolean,
observeEvenIfShallow = false
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if (
(!getter || setter) &&
(val === NO_INITIAL_VALUE || arguments.length === 2)
) {
val = obj[key]
}
// 调用observe方法,兼容数组的响应式情况
+ let childOb = shallow ? val && val.__ob__ : observe(val, false, mock)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
if (__DEV__) {
dep.depend({
target: obj,
type: TrackOpTypes.GET,
key
})
} else {
dep.depend()
}
+ if (childOb) {
+ childOb.dep.depend()
+ if (isArray(value)) {
+ dependArray(value)
+ }
+ }
}
return isRef(value) && !shallow ? value.value : value
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val
if (!hasChanged(value, newVal)) {
return
}
if (__DEV__ && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else if (getter) {
// #7981: for accessor properties without setter
return
} else if (!shallow && isRef(value) && !isRef(newVal)) {
value.value = newVal
return
} else {
val = newVal
}
childOb = shallow ? newVal && newVal.__ob__ : observe(newVal, false, mock)
if (__DEV__) {
dep.notify({
type: TriggerOpTypes.SET,
target: obj,
key,
newValue: newVal,
oldValue: value
})
} else {
dep.notify()
}
}
})
return dep
}
对应的observe
的函数
diff
/src/core/observer/index.ts
export function observe(
value: any,
shallow?: boolean,
ssrMockReactivity?: boolean
): Observer | void {
// 若有无__ob__属性直接返回
+ if (value && hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
+ return value.__ob__
+ }
if (
shouldObserve &&
(ssrMockReactivity || !isServerRendering()) &&
(isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value.__v_skip /* ReactiveFlags.SKIP */ &&
!isRef(value) &&
!(value instanceof VNode)
) {
// 否则创建并返回Observer实例
+ return new Observer(value, shallow, ssrMockReactivity)
}
}
4. 发送通知
如下在拦截器中,通·this.__ob__
拿到Observer实例,通过ob.dep.notify
通知所有依赖进行更新
diff
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto);
['push',
'pop',
'unshift',
'shift',
'splice',
'sort',
'reverse'].forEach(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 result
})
})
5. 递归侦测数组中的所有元素
通过observeArray
方法,将数组的每一项数据转化成响应式
diff
class Observer {
constructor (value) {
this.value = value
this.dep = new Dep()
// 将__ob__属性存到Observer实例上
def(value, '__ob__', this)
// 如果是数组
if (Array.isArray(value)) {
// 覆盖数组的原型
// value.__proto__ = arrayMethods
// 这样也可以实现
// Object.setPrototypeOf(value, arrayMethods)
// 修改为
const augment = hasProto ? protoAugment : copyAugment
augment(value, arrayMethods, arrayKeys)
+ this.observeArray(value)
}
// 如果是对象
else {
this.walk(value)
}
}
walk (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
+ observeArray (items) {
+ // 数组所有子数据转化为响应式
+ for (let i = 0, l = items.length; i < l; i++) {
+ observe(items[i])
+ }
+ }
}
6. 侦测新增数据
如下,如果是新增元素会用到push unshift splice
等方法,如果inserted
有值,则调用observeArray
对新增的数据进行遍历
diff
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto);
['push',
'pop',
'unshift',
'shift',
'splice',
'sort',
'reverse'].forEach(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
})
})
splice方法的处理是,如果调用list.splice(0, 1, 33)
,则args
是[0, 1, 33]
,inserted
值就是33,此时ob.observeArray
就会对33进行判断是否要转化为响应式数据
对应源码
js
// src/core/observer/array.ts
methodsToPatch.forEach(function (method) {
// cache original 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
if (__DEV__) {
ob.dep.notify({
type: TriggerOpTypes.ARRAY_MUTATION,
target: this,
key: method
})
} else {
ob.dep.notify()
}
return result
})
})
7. 不足
目前的实现,无法拦截直接通过length修改
者是通过索引修改
情况,只实现了对数组原型方法的拦截
js
this.list.length = 0
this.list[0] = 1