Vue2关于数组和对象新增删除属性的响应式机制

989 阅读3分钟

关于Array和纯粹Object类型的响应式注册,Vue里面比较巧妙。
1、Array响应式注册源码解析
Array对象有push pop shift unshift reverse sort splice7种方法会改变原值。但是如何给这7种方法实现响应式机制呢?源码如下:

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(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); }
    // notify change
    ob.dep.notify();
    return result
  });
});
  1. Array类型的对象收集依赖时很简单,get方法中实现即可。但是触发依赖并不能使用set方法,也不能使用a[0]=1这种方式,须使用相关API
  2. 所以在7种方法触发的时候,触发依赖就可以了
  3. Vue实现了对Array原型方法的重写,这可以避免对Array原型的修改(因为也不建议直接修改原型对象),同时也可以保留数组的其他属性
    1. 首先新建一个对象arrayMethods,该对象继承Array.prototype,所以arrayMethods就有了数组原型对象的所有属性
    2. 然后对arrayMethods的7种方法重写,使用Object.defineProperty,把方法赋值给arrayMethods,每次数组对象调用这7种方法时,就会调用mutator
    3. mutator方法,主要做了以下几件事情:
      1. 把7种方法绑定在arrayMethods,也就是original.apply(this, args)
      2. 若有新增元素,须对新增元素收集依赖,所以对push unshift splice特殊处理
      3. 最后触发变更,也就是ob.dep.notify()
    4. arrayMethods实则是一个原型对象,下一步只需要把arrayMethods赋值给数组对象的__proto__即可
    5. 原型对象绑定代码如下:
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);
  }
};

如果数组对象没有__proto__,就把arrayMethods的每个方法赋值给该对象的原型,且这些方法是不可枚举的 如果数组对象存在原型,直接arrayMethods替换原型就可以了。

2、Object响应式注册源码解析
关于Object响应式注册机制,对于一般化的赋值,触发get和set方法即可。但是Object新增属性和删除属性是另一种实现方式,这里仅介绍这两种方式的源码:

function set(target, key, val) {
  if (isUndef(target) || isPrimitive(target)
  ) {
    warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target))));
  }
  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
  }
  var ob = (target).__ob__;
  if (target._isVue || (ob && ob.vmCount)) {
    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$$1(ob.value, key, val);
  ob.dep.notify();
  return val
}

function del(target, key) {
  if (isUndef(target) || isPrimitive(target)
  ) {
    warn(("Cannot delete reactive property on undefined, null, or primitive value: " + ((target))));
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1);
    return
  }
  var ob = (target).__ob__;
  if (target._isVue || (ob && ob.vmCount)) {
    warn(
      'Avoid deleting properties on a Vue instance or its root $data ' +
      '- just set it to null.'
    );
    return
  }
  if (!hasOwn(target, key)) {
    return
  }
  delete target[key];
  if (!ob) {
    return
  }
  ob.dep.notify();
}

Vue.prototype.$set = set;
Vue.prototype.$delete = del;

新增方法就是$set。
1、新增时,传入三个参数,分别是目标对象、新增属性名、属性值,如果属性名可以转换为合法的数字,且目标对象是数组,可以调用splice方法实现数组的新增;
2、如果新增属性是对象的已有属性,就赋值操作
3、后面这一段代码:

defineReactive$$1(ob.value, key, val);

4、如果新增了属性,必然要给新属性收集依赖,就是这个意思
5、最后触发依赖即可

删除方法就是$delete
1、删除,传入目标对象和属性两个参数,如果目标对象是数组且属性可以转换为索引,调用splice实现删除
2、如果该属性存在,调用delete删除对象属性
3、最后触发依赖即可