Vue中对数组特殊的操作

蚊子前端博客
发布于 2018-06-20 20:41
Vue中对数组类型的数组是怎么操作的呢?我们看下Vue源码中的实现

1. 为什么 Vue 中不能通过索引来修改数组以更新视图

我们知道在 Vue 中的数据是通过Object.defineProperty这种劫持的方式来实现数据更新的,可是数组是一个比较特殊的类型。官网上说:

由于 JavaScript 的限制,Vue 不能检测以下变动的数组: 当你利用索引直接设置一个项时,例如:vm.items[indexOfItem] = newValue 当你修改数组的长度时,例如:vm.items.length = newLength

我搜寻其他的文章里也说是因为大部分浏览器Object.observe()支持的不友好,不能检测很好的检测到数组的数据变化,因此在 Vue 中则使用了其他的方法来实现数据的更新。

那么我就在想,作者为什么不用 Object.defineProperty 来实现对数组的数据劫持呢,不能实现的原因是什么呢?我们这里来看个 demo:

COPYJAVASCRIPT

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,修改索引在 0-5 之间任意索引的数据,都能触发 set 方法,获取相应的数据时,就能触发 get 方法。不过当获取额外的数据时,或者先删除原数据再添加时,就不会触发相应的方法了:

COPYJAVASCRIPT

arr[6] = 6; console.log(arr[6]); // 6 delete arr[2]; arr[2] = 2; console.log(arr[2]);

Vue中对数组的操作-蚊子的前端博客

因为上面我们只对已存在的索引的数据进行数据劫持,新添加的数据或者先删除再添加的数据,都是没有 get 和 set 方法的,都不能通过数据劫持的方式来更新视图。但数组是正常更新的,只是无法更新视图了。

所以我在这里大胆的猜测一下作者不用索引来更新视图的原因:防止因新添加或者先删除再添加等的操作,导致后续添加的数据没有 get 和 set 方法,在 Vue 中就统一操作,无论是数组还是 Object 类型的数据,都可以通过Vue.$set(Array|Object, key, val)来操作数据,并更新视图(当然,Object 类型的数据,在 Vue 初始化时就已经存在的 key,是可以直接通过 key 来修改的哈)。

作者尤雨溪在 GitHub 上有专门的回复,为什么 vue 没有提供对数组属性的监听

因为性能问题,性能代价和获得的用户体验收益不成正比。

以下源码均摘自与 github 上的Vue.js v2.5.17-beta.0版本。 commit 链接: Vue.js v2.5.17-beta.0

2. 变异方法更新数组

在官方网站中,提供了以下几个方法来检测数组的变化,也会触发视图的更新:

  • push()

  • pop()

  • shift()

  • unshift()

  • splice()

  • sort()

  • reverse()

在 Vue 的源码中,对 Vue 中的数组数据实现了类似的方法,这里官方的叫法是变异方法,比如push()方法,并不是调用 Array 中原生的 push 方法,而是调用自己实现的 push 方法,来实现数据的劫持,再通过 Array 中的 push 方法实现数组数据的更新,可能有点晕,看代码,我这里把几个距离较远的代码都放到一块了,但代码都没动,只是改了位置:

COPYJAVASCRIPT

// 获取原生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]); } };

这里我们也就知道了,在 Vue 中的数组数据,调用上面的那些方法,都是事先实现的同名方法,然后再通过apply调用原生方法,再添加监听器,触发更新等后续操作。

如果我们自己想单独实现下这样的操作如何实现呢,如果把仅仅把上面的代码拿出来是不行的,这些自定义的变异方法还没有跟数组进行绑定呢,数组调用这些方法时,还是原生的方法,因此这里我们需要把这些变异方法绑定到数组上,我们来看下 Vue 源码中的实现:

COPYJAVASCRIPT

// 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]); } }

这样就能给每个数组添加上这些变异方法了。因此 Vue 中也可以使用splice()来更新数组中的内容:

COPYJAVASCRIPT

vm.$arr.splice(index, 1, newVal);

我们自己实现这样的操作时,可以把上面的 Observer 简化一下:

COPYJAVASCRIPT

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 的实现

不多说话,直接看源码怎么实现的,内部有本人的中文注释:

COPYJAVASCRIPT

/** * 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; }

因此使用 Vue.$set 方法更新数组时,内部依然是用的splice()方法来操作的,而更新对象类型的数据则略微复杂点。

本人学艺不精、才疏学全,如果还有什么疏漏的地方,还望批评指正!

标签:
阅读(4526)

公众号:

qrcode

微信公众号:前端小茶馆