前端笔记 - vue2.x data初始化以及Observer

1,581 阅读6分钟

前提:已经clone下来vue源码

vue的data属性是在实际使用中最常见的了,以及大家老生常谈的双向绑定。本篇文章介绍了data属性的初始化以及双向绑定中model绑定部分。

先概述一下整体的逻辑过程:

1、初始化vue实例

2、在实例created生命周期之前执行initState函数,这个函数对props、methods、data、computed、watch初始化

3、在data初始化中,vue会对data绑定一个观察者Observer,每次data中的字段更新后都会通知依赖收集器Dep触发响应式更新


接下来说正题:data是在new Vue中初始化的,看一下具体路径

1、new Vue初始化位置:/src/core/instance/index.js中的initMixin函数中

2、initMixin函数在/src/core/instance/state.js中调用了同文件中的initState函数

3、在initState函数中首先判断了用户是否已经配置了data属性,如下图

4、我们直接看一下initData函数


在我们使用vue的时候data可以有两种方式定义,一种是用函数返回一个对象,另一种是直接定义一个对象。

1、在initData函数中,首先判断了data类型是否为函数,如果为函数的话则调用getData函数只是.call一下函数返回对象,将对象首先复制到实例的_data上。

2、这里isPlainObject函数是通过调用data对象的toString函数判断是否为Object,它判断了_toString.call(obj) === '[object Object]'

3、keys是data中的所有字段、props是实例中的props、methods是实例中的methodes,while开始对data中每个key进行遍历,首先判断methods中是否有重名的key,然后再判断props中是否有重名的key。这里还会通过isReserved函数判断是用来判断是否用到了保留关键字符“_”和“$”

4、在key字符串中不含有关键字符,接下来会调用proxy函数,这个函数的工作就是设置好key的get、set属性并配置到vue实例上。这里我们看到proxy中vue.key的get、set方法实际获取、改变的都是vue._data.key。


双向绑定之Observer

接下来就是重头戏了,vue双向绑定中其中之一model绑定,接上文的源码最后一行回调用observe函数开始model绑定。在开始继续上文之前我们先看一下Observe都做了什么

先看看人家官方解释:观察者类附加到每个观察对象。 一旦连接,观察者将目标对象的属性键转换为收集依赖关系和分派更新的getter / setter。说一下这个Observer类:

1、constructor中初始化了value(data\key)、dep(依赖收集器)、vmCount(订阅此观察者的节点)

2、接下来调用了def函数,为data定义__ob__属性,可以理解为将data绑定了一个observer,__ob__指向了这个oberver

3、接下来会判断value是否为数组,我们先看一下value为对象的情况,它会调用this.walk函数:walk函数先获取对象的所有key,然后遍历key并执行defineReactive函数。


我们来看看defineReactive函数都干嘛了(一张图截不下就直接上源码吧)

/**
 * Define a reactive property on an Object.
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

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

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

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const 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) {
      const 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 (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

面试中、各种文章中说的双向绑定的get、set就在这个函数中实现了,让我们来解析一下吧

1、new了一个dep依赖收集(后面文章会提起)

2、通过getOwnPropertyDescriptor获取到data中key对应的访问器属性赋值给property,并判断当前key是否在data中并且是可配置的(configurable为true才可以)

3、获取访问器属性中的get、set属性,判断存在setter或不存在getter以及实参各户只有两个时将val为data中key对应的value(在walk中就是传了两个参数)

4、这里我们看到了shallow字段,这里我理解为对val的深度\浅度遍历(如果为对象的话)绑定一个观察者。那么由于walk函数中并没有传则默认!shallow为ture。observe后面我们在讨论

5、接下来就到了万人皆知的get、set了,先说get属性,我看到了它会先判断是否已经设置get属性(第3条中获取),如果有则call getter函数,如果没有则直接返回val值。接下来会判断Dep的target(它的作用后面文章会搭配watcher详细说),这里先理解为每次get的时候都会订阅到Dep的subs中,包括子元素、如果为数组的话则数组中每个子元素都会订阅到Dep的subs中。

6、再看看set属性,首先通过getter来获取到value,然后判断如果新的value与旧值===全等、新value与旧value的getter方法被重写过(每次get的value不一致)则不做任何事情,这也就是当我们this.sthKey = oldValue重复赋值相同,不会触发响应式的原因。

7、接下来判断了一下是否有用户自定义的setter,如果有则setter.call(),如果没有的话则是常规赋值操作。

8、这里我们看到它再次对新的value进行了一次深度遍历为每个子元素注册一个观察者。

9、最后就是dep.notify()了,这个函数就是在data中的数据set之后观察者告诉订阅者要更新了完成model to view的响应式操作。

10、到这里就将data中每个key的get、set属性进行重写结束了,双向绑定也完成了一半。

11、我们回头看一下如果是数组的话,vue是如何做响应式的呢?首先判断了hasProto这个是一个布尔型它返回了__proto__是否在对象中

这里augment会根据hasProto赋值上面两个不同的函数,protoAugemnt很简单就是将value的__proto__赋值arrayMethods(arrayMethods为Array.prototype),copyAugment则遍历arrayKeys(arrayKeys为arrayMethods的OwnPropertyNames)每一个key,调用def函数,为data定义key属性(也就是定义Array.prototype)。

接下来就是遍历整个数组,对每个子元素调用observe函数。


我们来看看observe函数的实现。

1、首先函数判断了value必须为对象且不为VNode节点

2、初始化一个Observer对象,判断value是否含有__ob__属性且该属性是Observer对象,如果均满足则将初始化的Observer对象指向了value的__ob__属性,其次判断shouldObserve(常量默认为true)、是否为服务端渲染、value为数组或为对象、value对象是否可扩展(isExtensible es6语法)、是否为vue实例,经过以上的判断后ob又指向了新Observer对象(由value初始化)

3、判断当前的asRootData当前的value是否为根结点的data,以及ob对象存在,则ob订阅此观察者的节点加一,最终返回ob对象。

以上就是笔者的理解,接下来会尽量将vue的各个模块都遍历到,如有任何建议欢迎提出。