Vue.js源码学习之响应式对象

224 阅读2分钟

回顾下上篇文章《Vue.js源码学习之依赖收集》中依赖收集的几个流程:

    1. 通过defineReactive设置普通对象为响应式对象,具体是在Object.defineProperty中通过getter方法收集依赖,通过setter方法通知依赖发生变化;
    1. 依赖收集的核心的Dep构造函数,两个核心方法depend()添加依赖,notify()通知依赖变化,每个Dep实例都有唯一id及依赖订阅数组subs,subs[]数组保存的是Watcher实例对象
    1. Dep是通过Wacher来完成依赖收集的。Dep.prototype.depend()是将Watcher实例对象推入到Dep的sub数组中,使watcher与dep建立起关联。响应对象数据发生变化的时候会执行Watcher.prototype.update()方法,从而触发对应的变化操作。

在前面文章《Vue.js源码学习之Vue的初始化》提到new Vue()的时候会完成initState初始化,包括data,props,computed,watch等等,初始化的过程实际上是将普通对象转换响应式对象的过程,那么Vue.js具体是如何完成这个过程的呢?我们以initData()方法逐行分析为例。

initData()

 function initData (vm) {
    var data = vm.$options.data;//组件的data属性,可以是对象也可以是一个返回对象的函数,一般我们使用函数。
    data = vm._data = typeof data === 'function'
      ? getData(data, vm)
      : data || {};
    if (!isPlainObject(data)) {//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);//获取data的key
    var props = vm.$options.props;//获取组件props对象
    var methods = vm.$options.methods;//获取组件中的方法
    var i = keys.length;
    while (i--) {
      var key = keys[i];//遍历data中的key
      {
        if (methods && hasOwn(methods, key)) {//method中的key不能和data中的key重名,否则报错
          warn(
            ("Method \"" + key + "\" has already been defined as a data property."),
            vm
          );
        }
      }
      if (props && hasOwn(props, key)) {//组件的props属性不能和data中的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);//通过代理,把props数据添加到vm对象上,使其也变成响应式
      }
    }
    // 核心方法,设置组件data数据为响应式
    observe(data, true /* asRootData */);
  }

如果组件中data属性是一个函数,会执行如下方法:

 function getData (data, vm) {
    // #7573 disable dep collection when invoking data getters
    pushTarget();//这里没有传target参数,初始化Dep的target为undefined
    try {
      return data.call(vm, vm);//此时data的this指向了vm对象实例,将data中的属性变为了当前vm实例的私有属性
    } catch (e) {
      handleError(e, vm, "data()");
      return {}
    } finally {
      popTarget();//把dep.target恢复到上一个状态
    }
  }

observe

通过逐行分析initData()方法,我们发现最后执行了observe方法,那么该方法是做什么的呢?

function observe (value, asRootData) {
    if (!isObject(value) || value instanceof VNode) {
      return
    }
    var ob;
    if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
      ob = value.__ob__;
    } else if (
      shouldObserve &&
      !isServerRendering() &&
      (Array.isArray(value) || isPlainObject(value)) &&
      Object.isExtensible(value) &&
      !value._isVue
    ) {
      ob = new Observer(value);
    }
    if (asRootData && ob) {
      ob.vmCount++;
    }
    return ob
}

observe的功能就是来监听数据的变化,它会给非Vnode对象添加一个Observer对象实例。

Observer

var Observer = function Observer (value) {
    this.value = value;//当前数据
    this.dep = new Dep();//创建新的依赖收集实例
    this.vmCount = 0;
    def(value, '__ob__', this);//执行 def 函数把自身实例添加到数据对象 value 的 __ob__ 属性上
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods);//保留数组原型
      } else {
        copyAugment(value, arrayMethods, arrayKeys);//复制数组元素
      }
      this.observeArray(value);//针对数组单独监听
    } else {
      this.walk(value);//普通对象直接遍历,调用defineReactive$$1方法,具体看下面代码
    }
};

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

observeArray

Observer.prototype.observeArray = function observeArray (items) {
    for (var i = 0, l = items.length; i < l; i++) {
      observe(items[i]);
    }
};

由于Object.defineProperty(obj,prop,descriptor)本身的局限性,参考Object.defineProperty(),所以对于数组,转为响应式数据的时候做了单独处理,相当于是针对数组元素递归调用observe()方法。

defineReactive?1

 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 函数最开始初始化 Dep 对象的实例,接着拿到 obj 的属性描述符,然后对子对象递归调用 observe 方法,这样就保证了无论 obj 的结构多复杂,它的所有子属性也能变成响应式的对象,这样我们访问或修改 obj 中一个嵌套较深的属性,也能触发 getter 和 setter。最后利用 Object.defineProperty 去给 obj 的属性 key 添加 getter 和 setter。

到此Vue.js把普通对象转为响应式对象的过程就分析完了,至于Dep和Watcher的实现,可参考我的另外一篇文章Vue.js源码学习之依赖收集