Vue2.0 源码分析笔记(六)props、methods、provide、inject初始化过程

props初始化过程

initprops 方法定义在src/instance/state.js中

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

以上代码简写为

if (!isRoot) {
  toggleObserving(false)
}
for (const key in propsOptions) {
  // 省略...
  if (process.env.NODE_ENV !== 'production') {
    // 省略...
  } else {
    defineReactive(props, key, value)
  }
  // 省略...
}
toggleObserving(true)

为了搞清楚其目的,我们需要找到 defineReactive 函数,注意如下高亮的代码:

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 省略...
//==============高亮=========
  let childOb = !shallow && observe(val)  
//==============高亮=========
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // 省略...
    },
    set: function reactiveSetter (newVal) {
      // 省略...
    }
  })
}

如上那句高亮的代码所示,在使用 defineReactive 函数定义属性时,会调用 observe 函数对值继续进行观测。但由于之前使用了 toggleObserving(false) 函数关闭了开关,所以上面高亮代码中调用 observe 函数是一个无效调用。所以我们可以得出一个结论:在定义 props 数据时,不将 prop 值转换为响应式数据,这里要注意的是:由于 props 本身是通过 defineReactive 定义的,所以 props 本身是响应式的,但没有对值进行深度定义。为什么这样做呢?很简单,我们知道 props 是来自外界的数据,或者更具体一点的说,props 是来自父组件的数据,这个数据如果是一个对象(包括纯对象和数组),那么它本身可能已经是响应式的了,所以不再需要重复定义。另外在定义 props 数据之后,又调用 toggleObserving(true) 函数将开关开启,这么做的目的是不影响后续代码的功能,因为这个开关是全局的。
最后大家还要注意一点,如下:

if (!isRoot) {
  toggleObserving(false)
}

这段代码说明,只有当不是根组件的时候才会关闭开关,这说明如果当前组件实例是根组件的话,那么定义的 props 的值也会被定义为响应式数据。

props 的校验

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

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
}

validateProp 一开始并没有对 props 的类型做校验,首先如果一个 prop 的类型是布尔类型,则为其设置合理的布尔值,其次又调用了 getPropDefaultValue 函数获取 prop 的默认值,而如上这段代码才是真正用来对 props 的类型做校验的。通过如上 if 语句的条件可知,仅在非生产环境下才会对 props 做类型校验,另外还有一个条件是用来跳过 weex 环境下某种条件的判断的,我们不做讲解。总之真正的校验工作是由 assertProp 函数完成的。

methods 选项的初始化及实现

methods 选项实现要简单的多,打开 src/core/instance/state.js 文件找到 initMethods 函数,如下:

function initMethods (vm: Component, methods: Object) {
  const props = vm.$options.props
  for (const key in methods) {
    if (process.env.NODE_ENV !== 'production') {
      if (methods[key] == null) {
        warn(
          `Method "${key}" has an undefined value 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] = methods[key] == null ? noop : bind(methods[key], vm)
  }
}

这样一来可以很清晰的看到 methods 选项是如何实现的,就是通过 for...in 循环遍历 methods 选项对象,其中 key 就是每个方法的名字。最关键的是循环的最后一句代码:

vm[key] = methods[key] == null ? noop : bind(methods[key], vm)

通过这句代码可知,之所以能够通过组件实例对象访问 methods 选项中定义的方法,就是因为在组件实例对象上定义了与 methods 选项中所定义的同名方法,当然了在定义到组件实例对象之前要检测该方法是否真正的有定义:methods[key] == null,如果没有则添加一个空函数到组件实例对象上。

provide 选项的初始化及实现

Vue.prototype._init 方法中的一段用来完成初始化工作的代码:

initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

可以发现 initInjections 函数在 initProvide 函数之前被调用,这说明对于任何一个组件来讲,总是要优先初始化 inject 选项,再初始化 provide 选项,这么做是有原因的,我们后面会提到。但是我们知道 inject 选项的数据需要从父代组件中的 provide 获取,所以我们优先来了解 provide 选项的实现,然后再查看 inject 选项的实现。
打开 src/core/instance/inject.js 文件,找到 initProvide 函数,如下:

export function initProvide (vm: Component) {
  const provide = vm.$options.provide
  if (provide) {
    vm._provided = typeof provide === 'function'
      ? provide.call(vm)
      : provide
  }
}

如上是 initProvide 函数的全部代码,它接收组件实例对象作为参数。在 initProvide 函数内部首先定义了 provide 常量,它的值是 vm.$options.provide 选项的引用,接着是一个 if 条件语句,只有在 provide 选项存在的情况下才会执行 if 语句块内的代码,我们知道 provide 选项可以是对象,也可以是一个返回对象的函数。所以在 if 语句块内使用 typeof 操作符检测 provide 常量的类型,如果是函数则执行该函数获取数据,否则直接将 provide 本身作为数据。最后将数据复制给组件实例对象的 vm._provided 属性,后面我们可以看到当组件初始化 inject 选项时,其注入的数据就是从父代组件实例的 vm._provided 属性中获取的。

以上就是 provide 选项的初始化及实现,它本质上就是在组件实例对象上添加了 vm._provided 属性,并保存了用于子代组件的数据。

inject 选项的初始化及实现

看完了 provide 选项的初始化及实现,接下来我们研究一下 inject 选项的初始化及实现。找到 initInjections 函数,它也定义在 src/core/instance/inject.js 文件,如下是 initInjections 函数的整体结构:

export function initInjections (vm: Component) {
  const result = resolveInject(vm.$options.inject, vm)
  if (result) {
    // 省略...
  }
}

找到 resolveInject 函数,它定义在 initInjections 函数的下方,如下是其函数签名:

export function resolveInject (inject: any, vm: Component): ?Object {
  if (inject) {
    // inject is :any because flow is not smart enough to figure out cached
    const result = Object.create(null)
    const keys = hasSymbol
      ? Reflect.ownKeys(inject)
      : Object.keys(inject)

    for (let i = 0; i < keys.length; i++) {
      const key = keys[i]
      // #6574 in case the inject object is observed...
      if (key === '__ob__') continue
      const provideKey = inject[key].from
      let source = vm
      while (source) {
        if (source._provided && hasOwn(source._provided, provideKey)) {
          result[key] = source._provided[provideKey]
          break
        }
        source = source.$parent
      }
      if (!source) {
        if ('default' in inject[key]) {
          const provideDefault = inject[key].default
          result[key] = typeof provideDefault === 'function'
            ? provideDefault.call(vm)
            : provideDefault
        } else if (process.env.NODE_ENV !== 'production') {
          warn(`Injection "${key}" not found`, vm)
        }
      }
    }
    return result
  }
}

keys 常量中保存 inject 选项对象的每一个键名,接下来的代码使用 for 循环,用来遍历刚刚获取到的 keys 数组,其中 key 常量就是 keys 数组中的每一个值,即 inject 选项的每一个键值,provideKey 常量保存的是每一个 inject 选项内所定义的注入对象的 from 属性的值,我们知道 from 属性的值代表着 vm._provided 数据中的每个数据的键名,所以 provideKey 常量将用来查找所注入的数据。最后定义了 source 变量,它的初始值是当前组件实例对象。接下来将开启一个 while 循环,用来查找注入数据的工作。
我们知道 source 是当前组件实例对象,在循环内部有一个 if 条件语句,如下:

if (source._provided && hasOwn(source._provided, provideKey))

该条件检测了 source._provided 属性是否存在,并且 source._provided 对象自身是否拥有 provideKey 键,如果有则说明找到了注入的数据:source._provided[provideKey],并将它赋值给 result 对象的同名属性。有的同学会问:“source 变量的初始值为当前组件实例对象,那么如果在当前对象下找到了通过 provide 选项提供的值,那岂不是自身给自身注入数据?”。大家不要忘了 inject 选项的初始化是在 provide 选项初始化之前的,也就是说即使该组件通过 provide 选项提供的数据中的确存在 inject 选项注入的数据,也不会有任何影响,因为在 inject 选项查找数据时 provide 提供的数据还没有被初始化,所以当一个组件使用 provide 提供数据时,该数据只有子代组件可用。
那么如果 if 判断条件为假怎么办?没关系,注意 while 循环的最后一句代码:

source = source.$parent

重新赋值 source 变量,使其引用父组件,以及类推就完成了向父代组件查找数据的需求,直到找到数据为止。
但是如果一直找到了根组件,但依然没有找到数据怎么办?
们知道根组件实例对象的 vm.$parent 属性为 null,所以如上 if 条件语句的判断条件如果成立,说明一直寻找到根组件也没有找到要的数据,此时需要查看 inject[key] 对象中是否定义了 default 选项,如果定义了 default 选项则使用 default 选项提供的数据作为注入的数据,否则在非生产环境下会提示开发者未找到注入的数据。

最后如果查询到了数据,resolveInject 函数会将 result 作为返回值返回,并且 result 对象的键就是注入数据的名字,result 对象每个键的值就是注入的数据。
此时我们已经通过 resolveInject 函数取得了注入的数据,并赋值给 result 常量,我们知道 result 常量的值有可能是不存在的,所以需要一个 if 条件语句对 result 进行判断,当条件为真时说明成功取得注入的数据,此时会执行 if 语句块内的代码。在 if 语句块内所做的事情其实很简单:

toggleObserving(false)
Object.keys(result).forEach(key => {
  /* istanbul ignore else */
  if (process.env.NODE_ENV !== 'production') {
    defineReactive(vm, key, result[key], () => {
      warn(
        `Avoid mutating an injected value directly since the changes will be ` +
        `overwritten whenever the provided component re-renders. ` +
        `injection being mutated: "${key}"`,
        vm
      )
    })
  } else {
    defineReactive(vm, key, result[key])
  }
})
toggleObserving(true)

就是通过遍历 result 常量并调用 defineReactive 函数在当前组件实例对象 vm 上定义与注入名称相同的变量,并赋予取得的值。这里有一个对环境的判断,在非生产环境下调用 defineReactive 函数时会多传递一个参数,即 customSetter,当你尝试设置注入的数据时会提示你不要这么做。

另外大家也注意到了在使用 defineReactive 函数为组件实例对象定义属性之前,调用了 toggleObserving(false) 函数关闭了响应式定义的开关,之后又将开关开启:toggleObserving(true)。前面我们已经讲到了类似的情况,这么做将会导致使用 defineReactive 定义属性时不会将该属性的值转换为响应式的,所以 Vue 文档中提到了:

提示:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。

当然啦,如果父代组件提供的数据本身就是响应式的,即使 defineReactive 不转,那么最终这个数据也还是响应式的。

你可能感兴趣的:(Vue2.0 源码分析笔记(六)props、methods、provide、inject初始化过程)