【Vue源码解析】data篇

245 阅读2分钟

initData

Vue通过initData方法实现data选项的初始化

function initData(vm) {
  var data = vm.$options.data;
  // 如果data是函数,则执行函数并将返回值赋给vm._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];
    {
      // 是否和methods中的字段有冲突
      if (methods && hasOwn(methods, key)) {
        warn(
          ("Method \"" + key + "\" has already been defined as a data property."),
          vm
        );
      }
    }
    // 是否和props中的字段有冲突
    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)) { // 是否含有$、_字符, Vue内置的一些字段以$和_开头
      // 将key的get、set操作代理到_data对象上  this[key] => this._data[key]
      proxy(vm, "_data", key);
    }
  }
  // observe data
  // 将data变为响应式的关键
  observe(data, true /* asRootData */);
}

Vue中data选项支持2种数据类型(函数和纯对象),initData首先会判断data是否为函数,如果是函数则将执行后的返回值赋给vm._data。data支持函数主要是为了让保证每个子组件的data都保持一份各自的引用。

接着遍历data上的所有key,并检测key是否合法

  1. methods中是否定义同名key
  2. props中是否定义同名key
  3. 判断是否和Vue内置的字段冲突

如果key合法,则执行proxy(vm, "_data", key)。proxy相关代码如下:

var sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
};

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

proxy方法会通过Object.defineProperty将key代理到vm._data上。

initData在最后会执行observe将data变为可响应式

observe

function observe(value, asRootData) {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  var ob;
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    // value已经observe过了,__ob__存储observer对象
    ob = value.__ob__;
  } else if (
    shouldObserve &&             // vue内部的全局字段,用于控制是否进行observe
    !isServerRendering() &&      // 不是服务端渲染
    (Array.isArray(value) || isPlainObject(value)) &&  // 只有数组和纯对象需要进行observe
    Object.isExtensible(value) &&   // 判断一个对象是否是可扩展的(是否可以在它上面添加新的属性)可以通过Object.preventExtensions()、Object.freeze() 以及 Object.seal()设置为不可扩展
    !value._isVue
  ) {
    ob = new Observer(value);
  }
  if (asRootData && ob) {
    ob.vmCount++;
  }
  return ob
}

observe会判断value是否满足以下几点条件:

  1. 不是VNode类型
  2. value.__ob__不是Observer的实例 (防止重复Observer)
  3. shouldObserve为true(Vue源码闭包内的“全局字段”,用于控制是否进行observe的开关)
  4. 排除服务端渲染
  5. 必须是数组或者纯对象
  6. 可扩展属性 (因为需要设置get、set)

满足条件的会value最终会执行new Observer(value)

构造函数Observer

var Observer = function Observer(value) {
  // 存储value值
  this.value = value;
  // 依赖管理相关
  this.dep = new Dep();
  this.vmCount = 0;
  // 添加不可枚举字段__ob__,__ob__就是Observer实例对象本身, 可以用于标记已经进行observer
  def(value, '__ob__', this);
  if (Array.isArray(value)) {
    // 数组的observer处理,主要是对arrayMethods方法的“拦截”
    if (hasProto) {
      // 存在__proto__, 则 value.__proto__ = arrayMethods
      protoAugment(value, arrayMethods);
    } else {
      // 通过Object.defineProperty添加arrayMethods
      copyAugment(value, arrayMethods, arrayKeys);
    }
    // 对数组中的每个值都执行observe方法
    this.observeArray(value);
  } else {
    // 纯对象处理
    this.walk(value);
  }
};

Observer构造函数的作用是将value包裹成一个具备响应式的对象。this.value用于存储包裹前原有的值, dep用于收集的依赖。接着添加了__ob__字段,并且值为Observer对象本身。

接着,构造函数Observer会根据value是数组还是对象进行不同的处理。

Observer处理对象

// value为纯对象
this.walk(value);

Observer.prototype.walk = function walk(obj) {
  var keys = Object.keys(obj);
  for (var i = 0; i < keys.length; i++) {
    defineReactive$$1(obj, keys[i]);
  }
};

执行walk(value) 会遍历对象的每个key,并执行defineReactive$$1

// 删减部分代码
function defineReactive$$1(
    obj,
    key,
    val,
    customSetter,
    shallow
) {
  // 每个key的闭包内都存在一个dep对象,该对象用于存储与当前key有关的观察者(Watcher实例)
  var dep = new Dep();
  // 对val值进行递归observe,返回val对象的observe对象
  var childOb = !shallow && observe(val);

  Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter() {
          // Dep.target是一个全局对象,如果有值,则表示当前需要被收集的依赖(Watcher实例)
          if (Dep.target) {
              // depend方法用于收集观察者对象(Watcher)
              dep.depend();
              if (childOb) {
                  childOb.dep.depend();
                  if (Array.isArray(value)) {
                      dependArray(value);
                  }
              }
          }
          return value
      },
      set: function reactiveSetter(newVal) {
          // ... 省略
          val = newVal;
          // 将newVal转换为可观测的对象(递归设置getter、setter)
          childOb = !shallow && observe(newVal);
          dep.notify();
      }
  });
}

当执行defineReactive$$1(obj,key)时, 每个key的闭包内都会生成2个变量depchildOb,这两个变量都是用于收集依赖相关,会被闭包一致引用。接着执行 Object.defineProperty对key的get、set方法进行拦截。

先来看get方法:首先会判断Dep.target变量是否存在,如果存在会进入if语句内进行依赖收集Dep.target是整个Vue源码内的“全局变量”, Vue会在需要进行依赖收集时, 将Dep.target赋值为当前依赖本身(实际上就是Watcher对象)。然后通过执行dep.depend收集依赖。

set方法:首先将新值newValue进行observe化, 这样做是为了新添加的属性也具备响应式。接着通过dep.notify执行依赖。(执行dep.depend收集的依赖)

Vue收集依赖的过程: 将Dep.target赋值为依赖本身,然后触发需要收集这个依赖的key的get方法, 每个key闭包内的dep和childOb.dep就会执行depend方法收集依赖。

举个例子: 通常我们会在vue模板中引用一些data的属性,当被引用的属性发生变化时,视图就会自动发生改变,vue是如何做到的呢?

大致过程: vue会在解析模板时生成一个render函数,通过执行render函数可以生成相应的dom结构。当render函数执行时, 由于render函数内部引用了data的一些属性,这会触发这些属性的get方法。在触发get方法之前,vue会将一个renderWatcher(用于视图更新的Watcher对象)赋值给Dep.target。这样,模板中引用的属性的各自闭包内的dep就通过depend方法会收集到这个依赖。当这些属性发生变化时会触发set方法, 会通过dep.notify通知renderWatcher进行视图更新。这样就形成了数据和视图的响应式。

dep是每个key都存在的,用来来收集当前key的依赖。而childOb并不是所有key都有值,childOb主要用于Vue.set中

Vue.set原理

示例

 <div id='app'>
   {{person.address}}
   {{list}}
 </div>
var vm = new Vue({
    el: "#app",
    data: {
        person: {
            name: "Bob",
            age: 21
        },
        list: [1, 2, 3]
    }
})

// 对象
vm.person.age = 22;                     // 1.触发视图更新
vm.person.address = 'China';            // 2.不会触发视图更新
Vue.set(vm.person, 'address', 'China')  // 3.触发视图更新

// 数组
vm.list[0] = 22;         //4.不会触发视图更新
vm.list.splice(2, 1, 22) //5.触发视图更新
Vue.set(vm.list, 0, 22)  //6.触发视图更新

在讲解上述几个示例之前,先补充一点前提知识:vue的模板会生成render函数, 而render函数会触发引用的data的get方法。视图能否根据data进行响应式更新,关键在于

vm.person.age = 22;会触发视图更新,因为age字段在Vue对data进行Observe化阶段,已经通过Object.defineProperty拦截set、get方法。当render函数执行,会通过depend方法收集依赖。

vm.person.address = 'China'; 并不会触发视图更新。虽然render函数执行时会触发address的get方法, 但由于address是新增字段。并没有拦截其get、set的操作, 导致对于address的任何get/set操作Vue都无法感知到。

因为在Proxy之前,Javascript中没有任何方法可以拦截对象新增属性的操作。Vue为了做到对新增属性拦截, 提供了Vue.set方法。

Vue.set(vm.person, 'address', 'China')可以触发视图, 其实现的关键在于childOb。先来看看vm._data结构:

vm._data = {
    list: [
        2, 3, 4, {
            age: 21,
            __ob__: list[3]的Observer对象
        }
    ],
    person: {
        age: 32,
        name: "Bob",
        __ob__: person的Observer对象
    },
    __ob__: 整个data的Observer对象
}

我们注意到data中的属性如果是一个对象或者数组(见observer方法第5点条件),就会存在一个__ob__。这个__ob__是在Observer构造函数中定义的,并且等于Observer对象本身。来看一下childOb的定义。

 var childOb = !shallow && observe(val);

先不用管shallow字段,shallow在执行Observer时并没有传值。所以childOb = observe(val), 如果val满足一定条件就会返回Observe对象。其中比较重要的条件就是val必须是纯对象或者数组。 以上述的person字段为例,childOb就是person这个对象对应的Observer对象,也就是vm._data.person.__ob__。person字段闭包内的childOb有值会执行childOb.dep.depend()

if(childOb){
  childOb.dep.depend();
}

我们先看一下Vue.$set的实现:

// 简化代码
Vue.$set = function(target,key,val){
  // 纯对象处理:
  var ob = (target).__ob__;
  // 保证新添加属性具备响应式
  defineReactive$$1(ob.value, key, val);
  // 触发依赖
  ob.dep.notify();
}

这里ob就是childOb, 当执行Vue.set(vm.person, 'address', 'China')时,ob对应的是vm._data.person.__ob__(也就是person字段闭包里的childOb)。ob.dep.notify触发的是person字段的childOb.dep收集的依赖。

我明明想触发address字段收集的依赖, 为什么要触发person字段的收集的依赖呢?

以模板{{person.address}}为例。在触发render函数时,不仅会触发address的get方法,也会触发person的get方法,这意味着address和person收集的依赖是一样的。事实上, person字段收集到的依赖总是包含address字段的依赖的, 因为address只是person的一个字段, 无论是触发address的get方法还是set方法,都会相应的触发person的get和set方法。

Observer处理数组

Observer中对于数组的处理如下

// 简化代码
function Observer(){
  // ...其他代码
  if (hasProto) {
    // 存在__proto__, 则 value.__proto__ = arrayMethods
    protoAugment(value, arrayMethods);
  } else {
    // 通过Object.defineProperty添加arrayMethods
    copyAugment(value, arrayMethods, arrayKeys);
  }
  // 对数组中的每个值都执行observe方法
  this.observeArray(value);
}

arrayMethods是以Array.prototype为原型创建的对象,这个对象上挂载了一些数组的方法 protoAugment和copyAugment两个方法目的是一致的,都是在value数组上添加arrayMethods对象上的一系列数组方法,从而拦截数组的方法。

在支持__proto__的环境中, vm.list.__proto__ === arrayMethods.__proto__ === Array.prototype, 当对vm.list调用数组方法如push方法时,执行的push是arrayMethods上定义的push而不是原生数组方法

Vue采用monkey-patching方式, 首先存储原生数组方法,然后重写数组方法。代码如下:

  // 存储数组原型,主要用于获取原生的数组方法
  var arrayProto = Array.prototype;
  // 以Array.prototype为原型创建对象arrayMethods
  var arrayMethods = Object.create(arrayProto);

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

  methodsToPatch.forEach(function (method) {
    // 存储原生的数组方法
    var original = arrayProto[method];
    // 重写arrayMethods对象上的一些数组方法,达到拦截的效果
    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__;
      // inserted表示数组中新增的元素组成的数组
      var inserted;
      switch (method) {
        case 'push':
        case 'unshift':
          inserted = args;
          break
        case 'splice':
          // splice新增的元素
          inserted = args.slice(2);
          break
      }
      // 对新加入数组的元素进行observe化
      if (inserted) { ob.observeArray(inserted); }
      // notify change
      // 通知相应的观察者
      ob.dep.notify();
      return result
    });
  });

重写除了会执行原生的数组方法外,最关键的是会执行ob.dep.notify来通知观察者。以Vue.set(vm.list, 0, 22)为例,ob就是·vm.list.ob·, 也既是list字段的在defineReactive$$1闭包内定义的childOb。

这里需要注意的点是对于数组新增元素时的处理,对于新增的子元素(inserted)需要执行observeArray(inserted)让其observe化。

Vue.set中对数组的处理如下:

// 简化代码
Vue.$set = function(target,key,val){
    // 数组响应式处理:
    // 通过splice修改元素,由于splice方法已经被vue拦截,会通过target.__ob__.dep.notify执行依赖
    target.splice(key, 1, val);
    return val
}

Vue.set对于数组的处理很简单,通过触发splice修改对应元素。由于splice已经被拦截,实际会触发arrayMethods.splice方法,最终会触发target.ob.dep.notify执行依赖。

接下来看数组的示例:

// 数组
vm.list[0] = 22;         // 4.不会触发视图更新
vm.list.splice(2, 1, 22) // 5.可以触发视图更新
Vue.set(vm.list, 0, 22)  // 6.可以触发视图更新
  • 第4点 由于数组的索引不具备响应式,不能像对象属性那样通过Object.defineProperty进行拦截。所以this.list[2]=22并不会更新视图。
  • 第5、6点最终都会调用被vue重写后的splice方法,通过执行vm.list.__ob__.dep.notify()来执行依赖触发视图更新。 和对象一样,数组子元素的get、set都会触发数组本身的get、set。

在get方法中,child.dep.depend()下方还存在以下代码:

// 数组的处理
if (Array.isArray(value)) {
    dependArray(value);
}

function dependArray(value) {
  for (var e = (void 0), i = 0, l = value.length; i < l; i++) {
    e = value[i];
    e && e.__ob__ && e.__ob__.dep.depend();
    if (Array.isArray(e)) {
      dependArray(e);
    }
  }
}

dependArray的作用是遍历数组value, 并递归调用让每个子元素dep.depend来收集依赖,让数组的每一个子元素收集到和数组本身相同的依赖。因为数组子元素的改变时,对于数组本身而言也意味着发生了改变。

看如下示例:

vm.$watch('list', {
    deep: true,
    handler() {
        console.log('trigger watch');
    }
})

Vue.set(vm.list[3], 'name', "Bob")  // trigger watch

如果没有dependArray, 将不会输出"trigger watch",因为不执行dependArray, 数组子元素将不会收集到list的依赖。

结语

本文大致讲解了一下Vue在data初始化中进行的工作,如有不对之处请多多指教!!!