Vue细节之$data的细节

前言

$data是Vue实例中的实例属性,表示Vue实例观察的数据对象。实际上在Vue官网对这部分有较为详细的描述,这里就不再赘述了(具体可看官网的描述Vue选项/数据)。本篇文章从源码层次来梳理$data背后的逻辑,实际上就是一个问题:

假设存在属性name,为什么修改vm.$data.name与vm.name可以达到相同效果?

实例属性$data

假设存在属性name,为什么修改vm.$data.name与vm.name可以达到相同效果?

以此问题为出发点,假设data中存在name,实际上就是关注于data初始化的源码逻辑。之前的文章(Vue实例创建之data处理和挂载)有关于这边处理逻辑的梳理,实际上data的处理在源码逻辑都在initData函数中,源码具体如下:

function initData (vm) {
     
	var data = vm.$options.data;
	data = vm._data = typeof data === 'function'
    	? getData(data, vm)
        : data || {
     };
    if (!isPlainObject(data)) {
     
    	data = {
     };
        warn(
          'data functions should return an object:\n' +
          'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
          vm
        );
    }
    // proxy data on instance
    var keys = Object.keys(data);
    var props = vm.$options.props;
    var methods = vm.$options.methods;
    var i = keys.length;
    while (i--) {
     
    	var key = keys[i];
        {
     
          if (methods && hasOwn(methods, key)) {
     
            warn(
              ("Method \"" + key + "\" has already been defined as a data property."),
              vm
            );
          }
        }
        if (props && hasOwn(props, key)) {
     
          warn(
            "The data property \"" + key + "\" is already declared as a prop. " +
            "Use prop default value instead.",
            vm
          );
        } else if (!isReserved(key)) {
     
          proxy(vm, "_data", key);
        }
      }
      // observe data
      observe(data, true /* asRootData */);
}

initData函数的具体逻辑可以分为如下几个部分:

  • 判断data属性是对象还是函数做相关处理,但是保证最后的结果必须是普通对象
  • 获取数据对象data中所有属性的key做遍历处理(校验methods、props中是否存在同名属性 & proxy函数)
  • 调用observe对data中所有属性以及子属性做数据劫持

我们知道Vue是通过Object.defineProperty来实现数据劫持的,该方法的语法是:

Object.defineProperty(obj, prop, descriptor)

该方法只会对对象属性做数据劫持,这样会导致如下3个问题:

  • 问题1:属性可能是对象即无法对当前劫持对象的子对象属性做劫持
  • 问题2:无法劫持动态添加的属性
  • 问题3:如果直接对对象重新赋值要如何处理
Vue针对问题1的处理:递归劫持

对于问题1,Vue源码中采用递归处理,对所有子对象属性都数据劫持。在源码中实际上就是在defineReactive$$1中多次调用observe函数来实现的,defineReactive$$1就是对data进行数据劫持处理的函数。

Vue针对问题2的处理:额外的API

针对无法对对象动态添加的属性做数据拦截的问题,Vue内部提供如下的API:

  • Vue.set(target, prop, value)
  • vm.$set(target, prop, value):Vue.set的别名而已

之前一些项目Code Review时看到使用Object.assign来动态添加属性,这种方式也是不行的。

Vue针对问题3的处理:多层代理

在使用Vue时针对整个对象的重新赋值是生效的,常见的两种做法:

// 方式1
this.obj = {
     a : 1};
// 方式2
this.obj = Object.assign({
     }, this.obj, {
     b: 1});

上面两种做法本质是相同的即对obj对象重新赋值处理。为什么动态添加属性不行而重新赋值就可以呢?这取决于Vue源码中对data的多层代理。
实际上对于数据对象data,Vue实例中存在$data、_data这两种相同意义的属性,而多层代理也是结合这两个属性来实现的。

$data的劫持

$data的数据劫持的具体处理在stateMixin中,主要逻辑如下:

 var dataDef = {
     };
 dataDef.get = function () {
      return this._data };
 dataDef.set = function () {
     
 	warn(
    	'Avoid replacing instance root $data. ' +
        'Use nested data properties instead.',
         this
     );
  };
 Object.defineProperty(Vue.prototype, '$data', dataDef);

由上面的逻辑可知:vm.$data 会调用原型链上的Vue.prototype.$data,而vm.$data.prop实际上就是调用vm._data.prop。

_data的劫持

_data的数据劫持的具体的处理在initData中遍历逻辑中即proxy函数的调用,具体逻辑如下:

function proxy (target, sourceKey, key) {
     
      sharedPropertyDefinition.get = function proxyGetter () {
     
        return this[sourceKey][key]
      };
      sharedPropertyDefinition.set = function proxySetter (val) {
     
        this[sourceKey][key] = val;
      };
      Object.defineProperty(target, key, sharedPropertyDefinition);
}

代码逻辑很清晰就是调用Object.defineProperty,而关键在于target、sourceKey属性。源码调用如下:

proxy(vm, "_data", key);

由此可知vm.prop会调用vm._data.prop。

总结

源码中选项data实际上在Vue实例中对应_data内部属性,而Vue对vm.$data.prop、vm.prop的做了一层数据劫持,底层都是调用_data对象上的属性。

你可能感兴趣的:(Vue相关,Vue选项/数据,Vue源码)