Vue 响应式原理剖析 - 数据更新常见问题
对象数据的某些修改无法被检听?
如下的一个场景, obj.message 赋值时能否被监听响应呢?
var vm = new vue({
data: {
obj: {
a: 1
},
},
template: '<div>{{ obj.message }}</div>'
});
vm.obj.message = 'modified';答案是不能被监听的。原因:对象属性的添加和删除无法被 Object.defineProperty 监听,正如前文所述,Vue 的数据响应式基于 Object.defineProperty 实现,因此也受限。
解决办法:Vue 提供了特定的方法 vm.$set(obj, propertyName, newValue) 来处理这种情况,至于该方法的具体逻辑,后面会详细展开说明。
数组数据的某些修改无法被监听?
如下的一个场景,三个赋值语句里面,哪个能被监听响应呢?
const vm = new Vue({
data: {
items: [1, 2, 3, 4, 5],
},
});
vm.items[1] = 8;
vm.items[5] = 6;
vm.items.length = 2;答案是三个操作都不能被监听到。原因:
- 第二个操作 vm.items[5] = 6 应该是比较明显的,与上面对象添加和删除属性类似,数组新添加的元素和删除元素无法被 Object.defineProperty 监听。
- 第三个操作 vm.items.length = 2 也是由于 Object.defineProperty 的限制,数组的长度直接修改也无法被监听。
- 最容易误判的可能是第一个操作 vm.items[1] = 8 ,一种较为常规的说法是 Object.defineProperty 不支持监听数组元素的变化,要验证这个说法可以直接用一个例子说明真实的情况。
如下的一个例子:
function definereactive(obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
console.log('读取 index ' + key, '当前值是 ' + val)
return val;
},
set(newVal) {
if (val === newVal) {
return;
}
val = newVal
console.log('修改 index ' + key, '新值是 ' + val)
}
})
}
const testArray = [1, 2, 3, 4, 5]
testArray.forEach((c, index) => {
defineReactive(testArray, index, c)
});
testArray[0] = 100;
testArray[5] = 600;
可以看到, 超出范围的数组元素操作 Object.defineProperty 确实无法监听,但范围内的元素重新赋值是可以被监听的。 那么为什么在 Vue 中对数组类型的 data ,直接使用下标赋值无法被监听呢?
答案是出于性能考虑,从上面的基础例子中可以看到, 对象和数组如果需要监听每个属性和元素,实际上是对每个属性或者元素进行 Object.defineProperty 劫持,对象是监听 key 而数组则是以数字下标作为 key ,数组的数据量可能会很大,因此 Vue 出于性能考虑,并没有对元素下标进行响应式处理。 作为补充,Vue 对数组原型链上的几个方法进行劫持,对于会导致元素新增的3个方法 push 、 pop 、 unshift 会在内部获取新增的元素,执行响应式的处理:
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
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)
ob.dep.notify()
return result
})
})与对象类似,如果需要为数字元素重新赋值,可以使用 vm.$set(arr, indexOfItem, newValue) 方法,这里展示一下 $set 的具体实现:
// 精简了非 production 逻辑
export function set (target: Array<any> | Object, key: any, val: any): 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
}主要的逻辑包括以下操作:
- 这个方法同时用于对象和数组,因此第一步会检验传入的 target 是否为数组,并且传入的 key (即数组的数字下标)是否符合数组的长度范围(如上面所述, Object.defineProperty 不支持劫持新添加的元素),符合的元素会调用 splice 插入到数组中,由于 splice 已经被劫持,新增加的元素会进行「响应式」处理。
- 判断如果 key 原先已存在,则无需再监听。
- 判断是根节点 Vue,即最外层的 Vue,或者已经有 __ob__ 属性(表示已经进行响应式处理,详情可以浏览前文),则无需再进行监听。
- 如果不符合前面的条件,则表明该属性需要执行「响应式」处理,会调用 defineReactive 方法(响应式数据封装的入口方法,详情可以浏览前文)。
原文 https://kayosite.com/vue-reactivity-common-problems.html
本文内容仅供个人学习/研究/参考使用,不构成任何决策建议或专业指导。分享/转载时请标明原文来源,同时请勿将内容用于商业售卖、虚假宣传等非学习用途哦~感谢您的理解与支持!