深入了解 Vue 响应式原理(数据拦截)

4,367 阅读5分钟

前言

在上一章节我们已经粗略的分析了整个的Vue 的源码(还在草稿箱,需要梳理清楚才放出来),但是还有很多东西没有深入的去进行分析,我会通过如下几个重要点,进行进一步深入分析。

  1. 深入了解 Vue 响应式原理(数据拦截)
  2. 深入了解 Vue.js 是如何进行「依赖收集」,准确地追踪所有修改
  3. 深入了解 Virtual DOM
  4. 深入了解 Vue.js 的批量异步更新策略
  5. 深入了解 Vue.js 内部运行机制,理解调用各个 API 背后的原理

这一章节我们针对1. 深入了解 Vue 响应式原理(数据拦截) 来进行分析。

initState

我们在上一章节中已经分析了,在初始化Vue实例的时候,会执行_init方法, 其中会执行initState方法, 这个方法非常重要, 其对我们new Vue 实例化对象时,传递经来的参数props, methods,data, computed,watch的处理。 其代码如下:

  function initState (vm) {
    vm._watchers = [];
    var opts = vm.$options;
    if (opts.props) { initProps(vm, opts.props); }
    if (opts.methods) { initMethods(vm, opts.methods); }
    if (opts.data) {
      initData(vm);
    } else {
      observe(vm._data = {}, true /* asRootData */);
    }
    if (opts.computed) { initComputed(vm, opts.computed); }
    if (opts.watch && opts.watch !== nativeWatch) {
      initWatch(vm, opts.watch);
    }
  }

这一章节,我们只分析对data的处理, 也就是initData(vm)方法, 其代码如下(删除了异常处理的代码):

  function initData (vm) {
    var data = vm.$options.data;
    data = vm._data = typeof data === 'function'
      ? getData(data, vm)
      : data || {};
 
    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 */);
  }

从上面的代码分析,首先可以得出如下一个

总结:

  1. data里面的key一定不能和methods, props里面的key重名
  2. proxy(vm, "_data", key);只是将data里面的属性重新挂载(代理)在vm实例上,我们可以通过如下两种方式访问data里面的数据, 如vm.visibility 或者vm._data.visibility效果是一样的。 observe(data, true /* asRootData */);是最重要的一个方法,下面我们来分析这个方法

observe

observe中文翻译就是观察, 就是将原始的data变成一个可观察的对象, 其代码如下(删除了一些逻辑判断):

  function observe (value, asRootData) {
    ob = new Observer(value);
  }

这个方法就是new了一个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);
    }
  };

这个方法里面有对Array做特殊处理,我们现在传递的对象是一个Object, 但是里面todos是一个数组,我们后面会分析数组处理的情况, 接下来调用this.walk方法,就是遍历对象中的每一个属性:

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

defineReactive?1方法通过Object.defineProperty来重新封装data, 给每一个属性添加一个getter,setter来做数据拦截

  function defineReactive?1 (
    obj,
    key,
    val,
    customSetter,
    shallow
  ) {
    var dep = new Dep();

    var property = Object.getOwnPropertyDescriptor(obj, key);
    if (property && property.configurable === false) {
      return
    }

    // cater for pre-defined getter/setters
    var getter = property && property.get;
    var setter = property && property.set;
    if ((!getter || setter) && arguments.length === 2) {
      val = obj[key];
    }

    var childOb = !shallow && observe(val);
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
        var value = getter ? getter.call(obj) : val;
        if (Dep.target) {
          dep.depend();
          if (childOb) {
            childOb.dep.depend();
            if (Array.isArray(value)) {
              dependArray(value);
            }
          }
        }
        return value
      },
      set: function reactiveSetter (newVal) {
        var value = getter ? getter.call(obj) : val;
        /* eslint-disable no-self-compare */
        if (newVal === value || (newVal !== newVal && value !== value)) {
          return
        }
        /* eslint-enable no-self-compare */
        if (customSetter) {
          customSetter();
        }
        // #7981: for accessor properties without setter
        if (getter && !setter) { return }
        if (setter) {
          setter.call(obj, newVal);
        } else {
          val = newVal;
        }
        childOb = !shallow && observe(newVal);
        dep.notify();
      }
    });
  }

defineReactive?1方法就是利用Object.defineProperty来设置data里面已经存在的属性来设置getter,setter, 具体getset在什么时候发挥效用我们先不分析。

var childOb = !shallow && observe(val);是一个递归调observe来拦截所有的子属性。

data中的属性todos是一个数组, 我们又回到observe方法, 其主要目的是通过ob = new Observer(value);来生成一个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);
    }
  };

这里可以看出对Array有特殊的处理,下面我们我们来具体分析protoAugment方法

protoAugment(数组)

protoAugment(value, arrayMethods);传了两个参数,第一个参数,就是我们的数组,第二个参数arrayMethods需要好好分析,是Vue中对Array的特殊处理的地方。

其源码文件在vue\src\core\observer\array.js下,

  1. 首先基于Array.prototype原型创建了一个新的对象arrayMethods
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
  1. 重写了Array如下7 个方法:
var methodsToPatch = [
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
  ];
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let 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
  })
})

总结:从上面可知, Vue只会对上述七个方法进行监听, 如果使用Array 的其他的方法是不会触发Vue 的双向绑定的。比如说用concat,map等方法都不会触发双向绑定。

this.$set

上面已经分析了Object,Array 的数据监听,但是上面的情况都是在初始化Vue实例的时候,已经知道了data中有哪些属性了,然后对每个属性进行数据拦截,现在有一种情况就是,如果我们有需要需要给data动态的添加属性,我们该怎么做呢?

Vue单独开放出了一个接口$set, 他挂载在vm原型上,我们先说下其使用方式是: this.$set(this.newTodo,"name", '30')

  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
  }

通过上面的分析,使用$set方法,需要注意如下几点:

  1. target 不能是undefined, null, string, number, symbol, boolean六种基础数据类型
  2. target 不能直接挂载在Vue 实例对象上, 而且不能直接挂载在root data属性上

$set最终调用defineReactive?1(ob.value, key, val);方法去动态添加属性, 并且给该属性添加gettersetter

动态添加的属性,同样也需要动态更新视图,则是调用ob.dep.notify();方法来动态更新视图

总结

  1. 如果data属性是一个Object, 则将其将其进行转换,主要是做如下两件事情:
  1. 给对象添加一个__ob__的属性, 其是一个Observer对象
  1. 遍历data的说有属性('key'), 通过Object.defineProperty 设置其gettersetter来进行数据拦截
  1. 如果data(或者子属性)是一个Array, 则将其原型转换成arrayMethods(基于Array.prototype原型创建的一个新的对象,但是重新定义了 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse')七个方法,来进行对Array的数据拦截(这也就是Vue 对数组操作,只有这七个方法能实现双向绑定的原因)

在这篇文章我们已经分析了Vue 响应式原理 , 我们接下来会继续分析深入了解 Vue.js 是如何进行「依赖收集」,准确地追踪所需修改