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:

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 方法。不过当获取额外的数据时,或者先删除原数据再添加时,就不会触发相应的方法了:

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 方法实现数组数据的更新,可能有点晕,看代码,我这里把几个距离较远的代码都放到一块了,但代码都没动,只是改了位置:

// 获取原生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 源码中的实现:

// 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()来更新数组中的内容:

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

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

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

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

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

标签:
阅读(4332) 评论(0)

公众号:

qrcode

微信公众号:前端小茶馆