为什么要使用这两个API

我们知道在Vue中,对象和数组在某些情况下无法触发响应式数据更新。比如:

const vm = new Vue({
  el: '#root',
  data: {
    price: 10,
  },
});
vm.price = 20; // 重新渲染视图
vm.discount = 10; // 并不是响应式的数据

或者另一种情况,直接通过数组的下标修改数组的某一项:

const vm = new Vue({
  el: '#root',
  data: {
    list: ['cat', 'dog', 'pig'],
  },
});
vm.list[1] = 'fish'; // 不会重新渲染视图

为了解决上面的问题,Vue给出了两个Api,分别是Vue.setvm.$set

使用方式

这两个API的区别是Vue.set是定义在构造函数上的,而vm.$set是定义在原型对象上的。使用方式如下:

Vue.set(target, key, value);
// 或者
vm.$set(target, key, value)

修改数组也是同样的方法,key就是下标。

不允许动态地添加根级响应的属性,必须要在初始化实例前声明好所有根级属性,哪怕是一个空值。

解析源码

Vue.setvm.$set指向的是一个方法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 (isUndef(target) || isPrimitive(target)) {
      warn(
        'Cannot set reactive property on undefined, null, or primitive value: ' +
          target
      );
    }
}

首先set方法会进行判断,传入的target是否是null、undefined或是原始类型(string, number, boolean, symbol)。如果是就抛出异常—— 无法将undefined,null或者原始类型target设置为响应式属性。

if (Array.isArray(target) && isValidArrayIndex(key)) {
      target.length = Math.max(target.length, key);
      target.splice(key, 1, val);
      return val;
}

第一行判断target是否是一个数组,并且key值是否是合法key。下面是检查的方法,相信都能看懂。

/**
 * Check if val is a valid array index.
 */
function isValidArrayIndex(val) {
  var n = parseFloat(String(val));
  return n >= 0 && Math.floor(n) === n && isFinite(val);
}

第二行将target.length设置为target.lengthkey最大的值。这是为了防止某些情况下会报错,比如: 设置的key值,大于数组的长度。

new Vue({
    el: '#root',
    data: {
        list: [1, 2]
    }
})
Vue.set(vm.list, 10, 'error');

第三行是一个splice方法,将key位置的值替换为val。注意:当调用splice的时候就会重新渲染新的试图。因为这是一个特殊的splice方法,Vue将其改写了,看下面源码:

  var arrayProto = Array.prototype;
  // 创建了一个以arrayProto为原型的对象。
  // 也就是 arrayMethods.prototype = arrayProto
  var arrayMethods = Object.create(arrayProto);

  var methodsToPatch = ['push','pop','shift','unshift','splice','sort','reverse'];
 
  /**
   * Intercept mutating methods and emit events
   */
  methodsToPatch.forEach(function (method) {
    // 缓存原始数组的方法
    var original = arrayProto[method];
    // def就是Object.definePropery 给对象定义上面7个方法
    def(arrayMethods, method, function mutator() {
      ...省略部分代码(感兴趣可以看源码)
      // 发送更新通知
      ob.dep.notify();
      return result;
    });
  });

这是Vue定义的7个对象原型上的方法。

if (key in target && !(key in Object.prototype)) {
  target[key] = val;
  return val;
}

上面代码的意思是如果target对象上已经存在key,且这个key不是原型对象上的属性。则直接将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;
}

__ob__指的是Observer对象,vmCount用来表示当前对象是否是根级属性。_isVue用来表示是否是Vue实例,用来避免被observed,存在于Vue实例上。那么这个if就是用来判断target是否是根级属性或是Vue实例。

 /**
  * 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.
  */

// 翻译: Observer这个类依"触摸"每个被观察的对象。一旦"触摸",
// observer将每个目标对象的属性转成getter/setter,收集所有依赖触发更新。

var Observer = function Observer(value) {
    ...省略
    this.vmCount = 0;
    def(value, '__ob__', this);
    ...省略
};

if (!ob) {
  target[key] = val;
  return val;
}
defineReactive$$1(ob.value, key, val);
ob.dep.notify();
return val;

第1行到第4行的意思是,如果target上不存在Observer对象(这说明target只是一个普通的对象,不是一个响应式数据),则直接赋值给key属性。
第5行,ob.value其实就target,只不过它是Vue实例上data里的已经被追踪依赖的对象。然后在这个被observed的对象上增加key属性。让key属性也设置getter/setter
第6行,让dep通知所有watcher重新渲染组件。

完整代码

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

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐