vuejs源码解剖 — Vue初始化之initState

1,495 阅读8分钟

内部函数_init在执行完生命周期钩子函数beforeCreate之后created执行之前主要执行下面三个函数:

initInjections(vm)
initState(vm)
initProvide(vm)

因为initInjections(vm)以及initProvide(vm)Vue在后来版本增加的,且里面涉及到响应式数据的处理函数,而initState(vm)又是响应式处理的核心,所以打算先跳过这两个函数,先一睹initState风采,等这个弄熟练后,这两个也就小菜一碟了。

Vue初始化之initState

老规矩,先上源代码,位置src/core/instance/state.js

export function initState (vm: Component) {
  vm._watchers = []
  const 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)
  }
}

首先初始化了一个内部数组_watchers,用于存放当前组件实例的观察者,之后定义一个变量ops指向当前组件合并后的配置内容。接着就是若当前组件存在props则调用initProps初始化props,若存在methods配置,则调用initMethods初始化methods选项,若存在data则调用initData初始化data选项,若data不存在,则直接初始化一个空对象并观测之,若存在computed则调用initComputed初始化computed,若存在watchwatch非火狐浏览器原生方法,则调用initWatch初始化watch

注意:初始化的顺序很重要,这就标志着后面初始化的选项名称不能与已初始化的重复。因为不论propsmethods还是computed选项,最终都可以通过this.xxx的方式调用,若名称重复则必然会出现意外问题。

接下来我们就一一拆解这五个方法initPropsinitMethodsinitDatainitComputedinitWatch、是如何初始化数据的。

initProps

老规矩,先上源码:

function initProps (vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent
  // root instance props should be converted
  if (!isRoot) {
    toggleObserving(false)
  }
  for (const key in propsOptions) {
    keys.push(key)
    const value = validateProp(key, propsOptions, propsData, vm)
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      const hyphenatedKey = hyphenate(key)
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        warn(
          `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
          vm
        )
      }
      defineReactive(props, key, value, () => {
        if (!isRoot && !isUpdatingChildComponent) {
          warn(
            `Avoid mutating a prop directly since the value will be ` +
            `overwritten whenever the parent component re-renders. ` +
            `Instead, use a data or computed property based on the prop's ` +
            `value. Prop being mutated: "${key}"`,
            vm
          )
        }
      })
    } else {
      defineReactive(props, key, value)
    }
    // static props are already proxied on the component's prototype
    // during Vue.extend(). We only need to proxy props defined at
    // instantiation here.
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}

回顾:在理解如何初始化props之前,先回忆一下props的数据格式:

props:{
  yourPropsName:{
    type:Number,
    default:''
  }
}

在合并章节,有这么一个函数normalizeProps(child, vm),目的是将开发者对于props的多种写法规范成统一格式。那么统一格式目的的好处是啥,现在出现了:方便初始化以及后期处理。

  • 首先定义一个变量propsData保存来自外界的组件数据,若没有则保存一个空对象;
  • 定义一个变量props,组件实例上再定义一个_props属性,他们具有相同的引用,指向一个空对象;
  • 定义一个变量keys以及vm.$options._propKeys,也具有相同的引用,指向一个空数组;
  • 定义一个常量isRoot,用于保存当前组件是否是根组件。即当前组件若没有父组件,那一定就是根组件。
  • 接下来我们看到for循环遍历propsOptions数据,在循环前后各自执行了toggleObserving函数。那么这个函数是干嘛用的呢?它其实是一个开关,只要当前组件是非根组件就会打开。它会修改src/core/observer/index.js文件中shouldObserve变量的值。这个开关的目的就是是否需要对props进行响应式处理。由于目前还没接触到响应式,所以暂时不做讲解,后面initData的时候会详细说明。

接下来就是for循环里都做了些啥。首先是将props中的key值保存在vm.$options._propKeys数组中。接下来执行这个

const value = validateProp(key, propsOptions, propsData, vm)

validateProp函数的主要目的就是用来检验propskey值是否符合预期,并返回对应的value值。接下来看下源码:

export function validateProp (
  key: string,
  propOptions: Object,
  propsData: Object,
  vm?: Component
): any {
  const prop = propOptions[key]
  const absent = !hasOwn(propsData, key)
  let value = propsData[key]
  // boolean casting
  const booleanIndex = getTypeIndex(Boolean, prop.type)
  if (booleanIndex > -1) {
    if (absent && !hasOwn(prop, 'default')) {
      value = false
    } else if (value === '' || value === hyphenate(key)) {
      // only cast empty string / same name to boolean if
      // boolean has higher priority
      const stringIndex = getTypeIndex(String, prop.type)
      if (stringIndex < 0 || booleanIndex < stringIndex) {
        value = true
      }
    }
  }
  // check default value
  if (value === undefined) {
    value = getPropDefaultValue(vm, prop, key)
    // since the default value is a fresh copy,
    // make sure to observe it.
    const prevShouldObserve = shouldObserve
    toggleObserving(true)
    observe(value)
    toggleObserving(prevShouldObserve)
  }
  if (
    process.env.NODE_ENV !== 'production' &&
    // skip validation for weex recycle-list child component props
    !(__WEEX__ && isObject(value) && ('@binding' in value))
  ) {
    assertProp(prop, key, value, vm, absent)
  }
  return value
}

initMethods

props初始化完毕后,紧接着初始化methods,因为这个不涉及到响应式处理,所以相对简单很多

function initMethods (vm: Component, methods: Object) {
  const props = vm.$options.props
  for (const key in methods) {
    if (process.env.NODE_ENV !== 'production') {
      if (typeof methods[key] !== 'function') {
        warn(
          `Method "${key}" has type "${typeof methods[key]}" in the component definition. ` +
          `Did you reference the function correctly?`,
          vm
        )
      }
      if (props && hasOwn(props, key)) {
        warn(
          `Method "${key}" has already been defined as a prop.`,
          vm
        )
      }
      if ((key in vm) && isReserved(key)) {
        warn(
          `Method "${key}" conflicts with an existing Vue instance method. ` +
          `Avoid defining component methods that start with _ or $.`
        )
      }
    }
    vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
  }
}

首先定义变量props指向vm.$options.props,接下来遍历methods对象,在非生产环境下对每一个方法进行合规检查。

  • 类型检查:检测value值是否函数。因为methods对象下规定的必须都是函数,否则打印警告;
  • 检测当前函数名称是否与props中的命名相冲突。因为props先初始化,每个props都会挂载在vm实例下,后来初始化者都需要确保名称不与已初始化的相冲突。例如props中一个变量是xxx,我们可以在实例中通过this.xxx的形式获取值,这个时候若methods中也出现一个xxx方法,则会覆盖之前的props,所以命名规则不冲突,这是规则之一;
  • 函数命名规则检测:函数名称不以_$开头命名,且不与实例上已经存在的命名冲突。什么意思呢?意思是_或者$Vue内部使用命名规则,不建议开发者也这么命名。如果一定要以_$开头也没问题,只要不与$data这些保留关键词冲突也行。

以上只是合规检测,接下来才是重头戏,也是为啥能通过this获取到methods遍历 Methods 对象,使用 bind 绑定函数的 this 指向 vm,vm就是当前组件实例。在绑定的时候又做了一层安全处理,确保当前内容一定是函数,否则只绑定一个空函数,忽略不规则的方式。

//例如开发者这么写methods
{
  methods:{
   getName:{}
 }
}
// 我们看到 getName 是一个对象,非函数类型
// 在开发环境下,浏览器会打印警告,告诉开发者不能这么命名,但这个时候开发者自我要求不够,懒的去处理
// 这个时候实际 build项目的时候,Vue还是会将 getName 初始化为一个函数,只是函数为一个空对象
// 为什么会这么做呢?因为不能让一粒老鼠屎坏了一锅粥,不能因为 getName 不是函数而导致整体的运行

initData

methods初始化完毕后,就到了初始化data数据的时候了,老规矩,先上源码:

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && 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
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && 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 */)
}

首先将vm.$options.data赋值给变量data,在前面合并章节我们知道data是一个函数,所以我们需要先将函数转换得到对象。接下来就在实例下定义一个_data用以保存函数data转换后的对象,转换对象用的是getData方法

export function getData (data: Function, vm: Component): any {
  // #7573 disable dep collection when invoking data getters
  pushTarget()
  try {
    return data.call(vm, vm)
  } catch (e) {
    handleError(e, vm, `data()`)
    return {}
  } finally {
    popTarget()
  }
}
  • 我们看到代码主要包含在一个try...catch...中,这就是为了防止出现异常错误,如果出现则直接返回一个空对象;
  • 真正通过调用方法获得对象的关键点就是data.call(vm,vm)
  • getData方法开头以及结尾包含了这么两个函数pushTarget()popTarget(),这么做的目的是为了防止初始化data的时候收集冗余依赖。 接下来就是这一段:
if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }

isPlainObject方法我们在工具篇已介绍过,这里的目的是合规检测,防止通过getData函数得到的不是纯对象,若如此则直接给data赋值一个空对象并在非生产环境打印警告。

接下来就是合规检测以及在实例对象vm上定义与data同名的属性,确保this.xxx能够访问到data对象下的值。

const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
  const key = keys[i]
  if (process.env.NODE_ENV !== 'production') {
    if (methods && hasOwn(methods, key)) {
      warn(
        `Method "${key}" has already been defined as a data property.`,
        vm
      )
    }
  }
  if (props && hasOwn(props, key)) {
    process.env.NODE_ENV !== 'production' && 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)
  }
}

data对象中的key值保存在数组keys中,然后遍历对象data,这次没有采用for in循环的方式。用的是先将数组keys的数量保存在变量i中,然后while循环递减的方式遍历data对象。主要做了三件事:

  • 1、确保data中的key值与methods对象中上定义的函数名不重复;
  • 2、确保data中的key值与props对象上定义的key值不重复;
  • 3、确保data对象上每一个key值的命名规则:不是以_开头或者$开头,因为这是Vue的命名规则。

合规检测data对象上的每个key值之后,就开始执行这句:proxy(vm,'_data',key),这是用来干嘛的呢?其实这就是能通过this.xxx能访问到datakey值的原理。这次我们不上源码,先举个简单的例子,看懂这个也就能理解proxy的原理了

let vm = {
  data:{
    name:'wang',
    sex:'man'
  }
}
console.log(vm.data.name);  // wang
console.log(vm.data.sex);   // man

每次访问data中的数据,都要是vm.data.xxx的形式,如果我要偷个懒,想以vm.namevm.sex的方式也能访问到结果,那该怎么实现呢?答案很简单,就是使用Object.defineProperty代理一个值即可:遍历vm.data,给vm对象也挂个相同名称的变量,返回vm.data对象下相同名称的值即可。

for(let key in vm.data){
  Object.defineProperty(vm,key,{
    get:function(){
      return vm.data[key]
    }
  })
}

相当于遍历vm.data对象中的每个值,复制了一份到vm对象下。直接打印vm对象看下结果

{
  data:{
    name:'wang',
    sex:'man'
  },
  name:'wang',
  sex:'man'
}

原理是不是已经看明白了,那么我们接下来继续看下proxy的调用方式:

proxy(vm,'_data',key)

细心的同学应该已经发现,在proxy代理之前已经使用getData方法,执行vm.data.call(vm,vm)函数得到一个纯对象,并赋值给vm._data了,所以开发者实例中的data数据大概是这种格式的:

const vm = {
  data(){
    return {
      name:'wang',
      sex:'man'
    }
  },
  _data:{
    name:'wang',
    sex:'man'
  }
}

这个时候我们再看下proxy的源码,它接受三个参数:

  • target就是我们的组件实例;
  • sourceKey就是实例中的_data
  • key就是_data对象中的每一个key值。
export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

我们再回头看下initData函数中的while循环,每一次的循环执行到proxy的时候,其实相当于是这样的

Object.defineProperty(vm,'name',{
  get:function(){
    return vm._data.name
  },
  set:function(val){
    vm._data.name = val;
  }
})

接下来才是数据真正响应式的开始:

observe(data,true)

那我们顺着这个方向看下observe源码,位置:core/observer/index.js

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  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函数接受两个参数:

  • 1、需要被响应式的对象(必需),格式为Array或者Object
  • 2、一个布尔值,标志着是否是根组件(非必需)。

真正的响应式处理其实是在Observer类中,observe可以认为是Observer的入口,主要做一些规范处理。接下来主要列一些什么情况下会被响应式,什么情况下避免被响应式:

  • 1、如果不是纯数组或者纯对象,则返回空,不继续执行;
  • 2、如果是VNode的实例,也返回空,不继续执行;
  • 3、如果当前要观测的数组或者对象拥有__ob__属性,并且__ob__Observer类的实例,则意味着当前value已被响应式处理过,为了避免重复观测,则直接返回value.__ob__
  • 4、当且仅当满足这五个条件的时候才会执行响应式:一、开关打开(shouldObservetrue);二、非服务端渲染;三、是数组或者纯对象;四、当前value可扩展;五、valueVue实例。

接下来我们继续看Observer,源码位置:core/observer/index.js

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    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)
    }
  }

  /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  /**
   * Observe a list of Array items.
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

讲了这么久,Observer构造函数才是真正将数据转换成响应式的开始。它拥有三个实例属性:valuedepvmCount,以及两个方法walkobserveArray。空讲概念有点乏味,我们以观测一个对象为例继续:

const data = {
  name:'wang'
}
new Observer(data)

我们知道在new实例化的时候,会默认执行构造函数的constructor方法,它会给当前对象增加一个不可枚举的__ob__对象(为啥要不可枚举?为了不影响实际开发,比如for..in..循环的时候不遍历到__ob__属性),对象上拥有value属性指向当前观测的对象,dep属性是一个收集依赖的框框,用于收集保存依赖对象,以及一个vmCount属性,默认为0。那么实例化后可观测的对象大致格式如下:

{
  name:'wang',
  __ob__:{      // __ob__ 在chrome调试工具下看到是半透明状态,因为它是不可枚举的
    value:data,
    dep:new Dep(),
    vmCount:0
  }
}

接下来就是判断当前观测对象value是数组还是对象,若是数组则调用observeArray方法进行响应式观测,此方法很简单,就是遍历数组每一项用observe方法重新走一遍流程。若是对象则调用walk方法进行响应式观测,原理也很简单,for循环遍历对象每一项,使用defineReactive方法观测,具体原理我们后面详细介绍。在继续响应式观测之前,我们先着重看下这一段:

if (hasProto) {
  protoAugment(value, arrayMethods)
} else {
  copyAugment(value, arrayMethods, arrayKeys)
}

当观测对象是数组的时候,有这么一个判断。hasProto在工具篇已经有所介绍,它是一个布尔值,源码是'__proto__' in {},目的是用来判断当前宿主环境是否支持__proto__属性。我们知道实例的__proto__往往指向原型对象,例如:

var arr = new Array();
console.log(arr.__proto__ === Array.prototype);   // true

所以当观测对象是数组的时候,正常情况下它的原型链应该是Array.prototype,上面if...else...的目的就是将要观测的数组的原型指向arrayMethods。若当前宿主环境支持__proto__属性, 则执行protoAugment(value, arrayMethods)修改原型链指向,否则调用copyAugment(value, arrayMethods, arrayKeys)修改原型指向。那么我们看下这两个方法的源码:

function protoAugment (target, src: Object) {
  target.__proto__ = src
}
function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

protoAugment方法很简单,直接修改实例的__proto__属性,也就是value.__proto__ = arrayMethods。若当前宿主环境不支持__proto__,则调用copyAugment方法,其中遍历每一项keys通过Object.defineProperty拦截指定方法。

数组拦截方法的实现

数组是一个特殊的数据结构,他有很多方法,并且有些方法会修改数组本身,这些方法被称为变异方法,这些方法有pushpopshiftunshiftsplicesortreverse,当开发者调用这些方法修改data中的数组数据的时候,Vue需要及时得知并作出反应。那么Vue究竟是如何做到的呢?我们先来看下如下源码src/core/observer/array.js

import { def } from '../util/index'

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

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

首先定义变量arrayProto指向数组的原型链Array.prototype,接着通过Object.create方法定义一个原型链指向Array.prototype的对象arrayMethods。然后定义数组methodsToPatch保存7个数组的变异方法遍历执行。在forEach遍历执行前,arrayMethods只是一个空对象,每一次遍历则给arrayMethods对象上挂载一个变异方法。接下来我们看看forEach遍历都做了些啥。

  • 首先定义一个变量original缓存原生数组方法(毕竟不能修改数组原本的功能);
  • 我们先看defmutator方法,开头是先执行const result = original.apply(this, args)得到结果达到实际目的,最后在return result返回结果。虽然是拦截,但显然并不会影响数组原生的方法;
  • 关键就在中间那一段ob.dep.notify()会调用__ob__.depnotify方法去通知它的订阅者,告诉对方:我有变化了,你快更新吧;
  • 除了这些,中间还有这么一段,目的是判断数组是否有新增内容,如果有新增,那么新增的内容也需要调用observeArray方法转换成响应式。
let inserted
switch (method) {
  case 'push':
  case 'unshift':
    inserted = args
    break
  case 'splice':
    inserted = args.slice(2)
    break
}
if (inserted) ob.observeArray(inserted)

到这里,数组的拦截原理基本上弄明白了,我们接下来看看纯对象的响应式原理

纯对象的响应式原理

回到Observer构造函数,当被观测者是纯对象的时候会执行this.walk(value)walk的源码如下:

{
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
}

我们举个纯对象的观测例子:

const obj = {
  a:1,
  b:2
}

当执行walk的时候,其实就是分别这么执行:

defineReactive(obj,'a');
defineReactive(obj,'b');

接下来看看defineReactive源码是怎么实现的,位置:core/observer/index.js

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()
      }
      // #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()
    }
  })
}

该函数的核心就是将对象的数据属性转换为访问器属性,也就是为数据对象的属性设置一对getter/setter。该函数最多接受5个参数。首先定义一个常量dep = new Dep(),该常量在后面访问器属性的getter/setter中被闭包引用,这个时候就能明白了数据对象中每个数据字段都通过闭包引用了属于自己的dep常量,每个字段的Dep对象都被用来收集那些属于对应字段的依赖。之后是这样一段代码:

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

定义变量property保存当前属性的描述对象,若当前对象的属性描述不可以被改变或者属性不可被删除时,则返回,停止执行。

const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
  val = obj[key]
}

接下来缓存原有描述属性中可能存在的get方法和set方法,分别缓存在变量gettersetter中,为什么要缓存呢?主要是接下来会执行Object.defineProperty重新定义属性的getter/setter,这会导致原有的getset方法被重新覆盖。接下来就是一个边界处理问题,当传的参数只有2个的时候,且不存在getter或者有setter的情况下,重新定义val的值。
接下来是这样一段代码:

let childOb = !shallow && observe(val)

即若shallowfalse,也就是!shallow为真的情况下,深度观测val。默认情况下shallow值不传,也就是undefined,也就是说默认情况下是开启深度观测的。

接下来我们看看拦截器的get方法都做了些啥:

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
}

因为是getter读取属性,首先要确保不能影响正常逻辑,要正确的返回结果,所以首先获取结果保存在变量value中,最后return返回正确的属性值。除了正确的返回属性值,就是收集依赖,这个才是getter的主要使命。接下来我们看看if语句都做了些啥。
Dep.target为真,则执行dep.depend()。那么Dep.target是什么?其实就是观察者,也就是说若当前属性被使用了,则添加依赖dep.depend(),由于目前还没讲到Dep构造函数,所以暂时认为只要执行了dep.depend()则就是收集依赖了,后面我们会详细讲Dep构造函数。

dep.depend()后我们看到还有这么一段代码:

if (childOb) {
  childOb.dep.depend()
  if (Array.isArray(value)) {
    dependArray(value)
  }
}

也就是说,若childOb存在,则会深度收集依赖。默认defineReactive是不传shallow参数的,那么childOb的值就是observe(val)的返回值,我们知道对象是key:value的格式,若value是基本数据类型,虽然执行了observe方法,但是我们回忆一下observe方法的第一句:

if (!isObject(value) || value instanceof VNode) {
  return
}

不是数组或者纯对象,就直接返回不执行了。若是纯对象,例如这种格式:

const data = {
  a:1,
  b:{
    c:3,
    __ob__:{
      dep,
      value,
      vmCount
    }
  },
  __ob__:{
    dep,
    value,
    vmCount
  }
}

这个时候若执行到defineReactive(data,'b')的时候,由于value{c:3},是纯对象,childOb的值就是__obj__:{dep,value,vmCount}了,那么childOb.dep.depend()就相当于data.b.__ob__.dep.depend(),进行深度依赖收集。接下来就是判断value类型,如果是数组的话,则调用dependArray函数逐个触发数组每个元素的收集依赖。

以上就是如何在get中收集依赖,我们接下来看看set中如何触发依赖,也就是当属性被修改的时候,如何及时通知观察者去更新数据。

以下是拦截器set函数的源码:

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()
  }
  // #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()
}

set函数主要做两件事:1、正确的为属性设置新值;2、触发相应的依赖;

首先是这两行代码:

const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
  return
}

获取属性的原有值保存在变量value中,若新值与旧值全等,则表示无需处理,直接返回。另外一个情况看起来比较绕,其实主要是为了处理新旧值都是NaN的情况。我们知道在浏览器中NaN不全等于自身

console.log(NaN === NaN);  // false

接下来就是在非生产环境下,若customSetter参数存在,则执行customSetter()。可以理解为辅助回调函数,只有在非生产环境下才会执行。

接下来是这样一段代码:

if (getter && !setter) return
if (setter) {
  setter.call(obj, newVal)
} else {
  val = newVal
}

如果只存在getter,且不存在setter的时候,直接返回,后续不再执行。接下来就是一个if...else...判断,目的是正确的设置属性值。setter常量保存的是属性原来自身拥有的set函数,若存在则调用原有方法设置新属性,若不存在,则直接val = newVal设置。

最后是这么两行代码:

childOb = !shallow && observe(newVal)
dep.notify()

默认shallow值不存在,也就是说会执行observe方法,对新添加的值进行深度观测。最后就是通知依赖,告诉观察者:我更新了,你们快跟着更新吧。