vue2 怎么监听数组变化

Vue2.x 不能检测以下数组的变动:1.直接修改数组长度;2.通过数组下标赋值。第一点主要是收到defineProperty的限制,第二点要究其原因,还是得从源码分析。

直接修改数组长度

Vue2.x 利用Object.defineProperty劫持对象的访问器,在属性值发生变化时我们可以获取变化,实现对数据变化的双向绑定。但是Object.defineProperty并不能监控数组长度的变化,因为数组对象中的 length 属性的 configurablefalse,不允许访问器对该属性进行操作

关于数据双向绑定,可以参考基于 defineProperty 和 proxy 实现数据双向绑定

通过数组下标赋值

vue 怎么监听数组

Observer,是实现 vue 响应式的核心模块,用于监听对象和依赖追踪

// Observer 监听对象
var Observer = function Observer(value) {
  this.value = value;
  this.dep = new Dep();
  this.vmCount = 0;
  def(value, "__ob__", this);
  if (Array.isArray(value)) {
    // 将重写的数组方法挂载到数组原型对象上
    if (hasProto) {
      protoAugment(value, arrayMethods);
    } else {
      copyAugment(value, arrayMethods, arrayKeys);
    }
    this.observeArray(value);
  } else {
    this.walk(value);
  }
};

可以看到,observer 遇到数组会走单独的处理逻辑,首先将重写的数组方法挂到数组原型方法上,然后进一步通过observeArray处理数组

// observeArray
Observer.prototype.observeArray = function observeArray(items) {
  for (var i = 0, l = items.length; i < l; i++) {
    observe(items[i]);
    // 如果items[i]是数组,会新建 Observer 对象,然后继续执行observe, 直到遍历完毕
  }
};
// observe
function observe(value, asRootData) {
  if (!isObject(value) || value instanceof VNode) {
    // 如果要观测的数据不是一个对象或者是 VNode 实例,则不做监听
    return;
  }
  var ob;
  if (hasOwn(value, "__ob__") && value.__ob__ instanceof Observer) {
    // 已经添加过observer实例,直接返回
    ob = value.__ob__;
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value);
  }
  if (asRootData && ob) {
    ob.vmCount++;
  }
  return ob;
}

可以看到,vue 并没有对数组的所有元素进行监听,所以通过数组下标修改或增加元素,会发现依赖该数组元素的视图并不会更新

重写数组方法

Vue 2.x 针对数组的部分原型方法做了特殊处理,通过调用这些重写的方法,可以对新增的元素的索引进行监听,确保新增元素是响应式的

var methodsToPatch = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "sort",
  "reverse",
];

methodsToPatch.forEach(function (method) {
  var original = arrayProto[method];
  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;
    }
    if (inserted) {
      ob.observeArray(inserted);
    }
    // 重新触发依赖更新
    ob.dep.notify();
    return result;
  });
});

通过源码可以看到,在操作数组时,如果使用 push、unshift、splice 添加或修改元素,会重新触发依赖的更新函数,渲染对应视图或对象

Vue.set

除了上述方法,我们还可以通过Vue.set来实现对数组的更新,并渲染对应视图。贴一波 set 函数源码,源码分析可以看下注释

function set (target, key, val) {
  ......
  if (Array.isArray(target) && isValidArrayIndex(key)) { // 处理数组元素
    target.length = Math.max(target.length, key)
    // 使用重写过的splice更新数组
    target.splice(key, 1, val)
    return val
  }
  if (key in target && !(key in Object.prototype)) {
    // 是对象原有属性,赋值即可触发更新函数
    target[key] = val
    return val
  }
  const ob = target.__ob__
  // __ob__指向target的observer实例
  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.'
    )
    // 对于已经创建的实例,Vue 不允许动态添加根级别的响应式属性
    // 所以会直接返回值
    return val
  }
  // target非响应式对象,不用对属性监听,直接赋值并返回
  if (!ob) {
    target[key] = val
    return val
  }
  // 给新增的对象属性添加依赖
  defineReactive(ob.value, key, val)
  // 触发依赖,重新渲染页面
  ob.dep.notify()
  return val
}