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

前言

在上一章节我们已经粗略的分析了整个的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 是如何进行「依赖收集」,准确地追踪所需修改

猜你喜欢

转载自juejin.im/post/5c47cbe95188252420629eca