我们知道在 Vue 中的数据是通过 由于 JavaScript 的限制,Vue 不能检测以下变动的数组: 我搜寻其他的文章里也说是因为大部分浏览器 那么我就在想,作者为什么不用 Object.defineProperty 来实现对数组的数据劫持呢,不能实现的原因是什么呢?我们这里来看个 demo: arr 数组的长度为 6,修改索引在 0-5 之间任意索引的数据,都能触发 set 方法,获取相应的数据时,就能触发 get 方法。不过当获取额外的数据时,或者先删除原数据再添加时,就不会触发相应的方法了: 因为上面我们只对已存在的索引的数据进行数据劫持,新添加的数据或者先删除再添加的数据,都是没有 get 和 set 方法的,都不能通过数据劫持的方式来更新视图。但数组是正常更新的,只是无法更新视图了。 作者尤雨溪在 GitHub 上有专门的回复,为什么 vue 没有提供对数组属性的监听: 因为性能问题,性能代价和获得的用户体验收益不成正比。 以下源码均摘自与 github 上的 在官方网站中,提供了以下几个方法来检测数组的变化,也会触发视图的更新: 在 Vue 的源码中,对 Vue 中的数组数据实现了类似的方法,这里官方的叫法是 这里我们也就知道了,在 Vue 中的数组数据,调用上面的那些方法,都是事先实现的同名方法,然后再通过 如果我们自己想单独实现下这样的操作如何实现呢,如果把仅仅把上面的代码拿出来是不行的,这些自定义的变异方法还没有跟数组进行绑定呢,数组调用这些方法时,还是原生的方法,因此这里我们需要把这些变异方法绑定到数组上,我们来看下 Vue 源码中的实现: 这样就能给每个数组添加上这些变异方法了。因此 Vue 中也可以使用 我们自己实现这样的操作时,可以把上面的 Observer 简化一下: 不多说话,直接看源码怎么实现的,内部有本人的中文注释: 因此使用 Vue.$set 方法更新数组时,内部依然是用的 本人学艺不精、才疏学全,如果还有什么疏漏的地方,还望批评指正!1. 为什么 Vue 中不能通过索引来修改数组以更新视图 #
Object.defineProperty
这种劫持的方式来实现数据更新的,可是数组是一个比较特殊的类型。官网上说:
当你利用索引直接设置一个项时,例如:vm.items[indexOfItem] = newValue
当你修改数组的长度时,例如:vm.items.length = newLengthObject.observe()
支持的不友好,不能检测很好的检测到数组的数据变化,因此在 Vue 中则使用了其他的方法来实现数据的更新。function defArray(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log('get val: ' + val);
return val;
},
set(newVal) {
console.log('old value: ' + val);
console.log('new value: ' + newVal);
val = newVal;
},
});
}
let arr = [0, 1, 2, 3, 4, 5];
arr.forEach((item, index) => {
defArray(arr, index, item);
});
arr[1] = 123; // 修改索引为1的值
/*
> arr[1] = 1
old value: 1
new value: 123
123
*/
console.log(arr[2]); // 获取索引为1的数据
/*
> console.log(arr[2]);
get val: 2
2
*/
arr[6] = 6;
console.log(arr[6]); // 6
delete arr[2];
arr[2] = 2;
console.log(arr[2]);
所以我在这里大胆的猜测一下作者不用索引来更新视图的原因:防止因新添加或者先删除再添加等的操作,导致后续添加的数据没有 get 和 set 方法,在 Vue 中就统一操作,无论是数组还是 Object 类型的数据,都可以通过Vue.$set(Array|Object, key, val)
来操作数据,并更新视图(当然,Object 类型的数据,在 Vue 初始化时就已经存在的 key,是可以直接通过 key 来修改的哈)。Vue.js v2.5.17-beta.0
版本。 commit 链接: Vue.js v2.5.17-beta.02. 变异方法更新数组 #
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
变异方法
,比如push()
方法,并不是调用 Array 中原生的 push 方法,而是调用自己实现的 push 方法,来实现数据的劫持,再通过 Array 中的 push 方法实现数组数据的更新,可能有点晕,看代码,我这里把几个距离较远的代码都放到一块了,但代码都没动,只是改了位置:// 获取原生Array中提供的所有方法
var arrayProto = Array.prototype;
// 将原生提供的方法创建一个新的对象,以免修改原生的方法,造成全局污染
var arrayMethods = Object.create(arrayProto);
// 将要实现的几个变异方法
var methodsToPatch = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
/**
* Intercept mutating methods and emit events
* 截取这些方法,然后实现相应的操作
*/
methodsToPatch.forEach(function (method) {
// cache original method
var original = arrayProto[method]; // 缓存原始方法
// def即Object.defineProperty的包装函数
def(arrayMethods, method, function mutator() {
var args = [],
len = arguments.length;
while (len--) args[len] = arguments[len];
var result = original.apply(this, args); // 调用原始方法完成对数组的更新
var ob = this.__ob__;
var inserted; // 存储要修改的数据
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
inserted = args.slice(2);
break;
}
console.log(args, inserted);
if (inserted) {
ob.observeArray(inserted);
} // 如果有修改的数据,则添加observer监听器
// notify change
ob.dep.notify(); // 触发更新
return result;
});
});
/**
* Observe a list of Array items.
* 对数组中的每项添加监听器
*/
Observer.prototype.observeArray = function observeArray(items) {
for (var i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
};
apply
调用原生方法,再添加监听器,触发更新等后续操作。// can we use __proto__?
var hasProto = '__proto__' in {}; // __proto__是否可用
/**
* Observer class that is attached to each observed
* object. Once attached, the observer converts the target
* object's property keys into getter/setters that
* collect dependencies and dispatch updates.
*/
var Observer = function Observer(value) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
def(value, '__ob__', this);
if (Array.isArray(value)) {
// 当前数据为数组,若__protp__可用,则调用protoAugment方法,否则调用copyAugment方法
var augment = hasProto ? protoAugment : copyAugment;
augment(value, arrayMethods, arrayKeys);
this.observeArray(value); // 监听数组的每一项
} else {
this.walk(value);
}
};
/**
* Augment an target Object or Array by intercepting
* the prototype chain using __proto__
* 把这些方法定义到原型链__proto__上
*/
function protoAugment(target, src, keys) {
/* eslint-disable no-proto */
target.__proto__ = src;
/* eslint-enable no-proto */
}
/**
* Augment an target Object or Array by defining
* hidden properties.
* ie9、ie10 和 opera 没有 __proto__ 属性,为数组添加自定义方法
*/
/* istanbul ignore next */
function copyAugment(target, src, keys) {
for (var i = 0, l = keys.length; i < l; i++) {
var key = keys[i];
def(target, key, src[key]);
}
}
splice()
来更新数组中的内容:vm.$arr.splice(index, 1, newVal);
function init(value) {
// can we use __proto__?
var hasProto = '__proto__' in {};
var augment = hasProto ? protoAugment : copyAugment;
augment(value, arrayMethods, arrayKeys);
}
var arr = [1, 2, 3, 4, 5];
init(arr);
arr.splice(2, 1, 2);
arr.push(6, 7, 8);
3. Vue.$set 的实现 #
/**
* Set a property on an object. Adds the new property and
* triggers change notification if the property doesn't
* already exist.
* 在一个对象上设置一个属性。如果该属性尚不存在,则添加新属性并触发更改通知。
*/
function set(target, key, val) {
if ('development' !== 'production' && (isUndef(target) || isPrimitive(target))) {
warn('Cannot set reactive property on undefined, null, or primitive value: ' + target);
}
// 如果当前对象是数组,且key是合法的数字索引
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 若索引值小于数组的长度,则数组长度不变,只修改
// 若索引值比当前数组的长度大,则说明是新添加元素,需要更新数组的长度
// 这里采用变异方法splice来更新数组
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
return val;
}
// 以下操作时,target均为object对象类型
// 若当前key已经存在于对象中,且没有原型链中,直接更新即可
// 因为在Vue初始化时已经添加了监听器,这里就不用再添加了
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val;
}
var ob = target.__ob__;
// 若当前对象是Vue实例,则不允许使用这种方式添加属性,应当在初始化时预先声明
if (target._isVue || (ob && ob.vmCount)) {
'development' !== '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;
}
// 若当前key不存在,则在ob.value上创建set和get方法,并触发更新
defineReactive(ob.value, key, val);
ob.dep.notify();
return val;
}
splice()
方法来操作的,而更新对象类型的数据则略微复杂点。
版权属于:
加速器之家
作品采用:
《
署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)
》许可协议授权
评论