Vue 2.x 源码解读系列《props 属性》

前言

本文主要是讲解 props 属性的初始化过程。

props 属性

在 init 方法中开始了 props 属性的初始化。但不是全部。

Vue.prototype._init = function (options?: Object) {
 ......
 initLifecycle(vm)
 // 添加事件监听
 initEvents(vm)
 // 在实例上挂载一些属性:_vnode,_staticTrees,$slots,$scopedSlots,$attrs,$listeners
 initRender(vm)
 // 触发 beforeCreate 钩子函数
 callHook(vm, 'beforeCreate')
 // 注入一些父级 provide(提供)出来的属性,inject 配全后面的 provide 就是我们平时在组件中解决跨多级组件通信问题的一种方法
 initInjections(vm) // resolve injections before data/props
 // 给实例添加 _watchers,使用方法 initProps(),initMethods(),initData(),initWatch() 
 // 分别初始化 props,methods,data,watch
 // 这就是为什么特定的属性或者方法只能在特定的钩子函数中才能访问到的原因
 initState(vm)
 // 向子孙组件提供数据,由于这个方法在 initState() 方法之后执行,所以我们可以把当前组件中的状态(如:props,methods,data)数据传到子子孙组件中。
 initProvide(vm) // resolve provide after data/props
 // 触发 created 钩子函数,当 created 钩子函数触发时,组件中所需要的东西(prop,data,methods,watch)已经创建完成
 callHook(vm, 'created')
 ......
 }

其中有一个方法 initState() 方法。

​
export function initState(vm: Component) {
 vm._watchers = []
 const opts = vm.$options
 // 如果组件中有 props 属性定义,则调用 initPropos() 方法初始化 props 属性
 // 从这里可以看出若子组件中没有定义任何 props 属性的话,是不会执行 initProps() 初始化的,不管你在父组件有没有传值。
 // opts.props 是经过处理的,不管我们使用何种方式定义 props 属性,最终 Vue 把它都转成 对象的形式来定义 props 属性 props: { name: { type:string } }。
 // 如果我们有设置了默认值,那么除了 type 属性外,还会有一个 default 属性。其它属性以此类推。
 if (opts.props) initProps(vm, opts.props)
 if (opts.methods) initMethods(vm, opts.methods)
 if (opts.data) {
  // 初始化 data
  initData(vm)
 } else {
  observe(vm._data = {}, true /* asRootData */)
 }
 if (opts.computed) initComputed(vm, opts.computed)
 // 如果组件中存在 watch ,并且不是原生的 watch,那么就初始化 watch
 // 这里的 watch 为我们在组件中定义的用来监听属性变化的那个 watch
 if (opts.watch && opts.watch !== nativeWatch) {
  // 传入实例以及组件中添加的 watch 对象
  initWatch(vm, opts.watch)
 }
}

可以看到在这个 initState() 方法中才真正地调用了一个名为 initPorps() 方法。首先在这个方法外包了一层判断。如果子组件中存在 props 属性,则调用 initState() 方法进行 props 相关属性的初始化。所以如果子组件中不需要传 props 属性,也不应该在子组件中加上空的 props 对象。

这里有一点需要注意的是我们给子组件传递属性的时候属性名的写法也是有讲究的。

那么下面我们来看看 initProps() 方法

function initProps(vm: Component, propsOptions: Object) {
 // vm.$options.propsData 为父组件传给子组件的属性
 // 注意:如果子组件的没有对应的 props 属性时,即使在父组件中给子组件传递了属性,也不会收集到 vm.$options.propsData 上
 // 如果子组件中字义了使用驼峰命名法书写的属性名,那么在父组件中需要使用-的方式,比如子组件中定义了 myName 属性,那么父组件给子组件传递属性的时候,这个属性名应该写成 my-name
 // 不管是 propsData 还是在 propsOptions 如果名字中存在 - 都会自动转成驼峰式命名法。
 // 在父组件中,如果在子组件元素上只添加 props 属性,或者传值为空,都会被转成空值 ''。
 // 如果父组件中有给子组件传递 props 属性,但子组件中没有定义对应的 props 属性,那么 vm.$options.propsData 也是没有值的。
 // 至于为什么,估计等到分析模板编译的时候应该会有答案
 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) {
  // 标记 shouldObserve 的值为 false
  toggleObserving(false)
 }
 // 遍历组件中已定义了的 props 属性
 for (const key in propsOptions) {
  // 缓存所有 props 的 key 到 vm.$options._propKeys 数据组里
  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
   )
  }
   // 第四个参数为一个函数,这个函数是在属性的值被修改的时候触发了 set 时调用的。
   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)
}

其中上面的 vm._props 对象的所有属性来自于 defineReactive(props, key, value) 方法处理后的结果(设置了get 和 se 拦截),即与 propsOptions 中的属性一一对应。只不过 vm._props 中的属性都是经过 defineReactive() 方法响应式处理的。

从这里我们可以知道,props 属性其实是可以更改的,并且它跟 data 中定义的属性一样会更新到页面中。只是在我们开始 时,会提示我们不要直接修改这个 props 属性。可以是可以这么做,只是官方不建议,因为在子组件中对 props 属性进行修改。会让数据变得难以理解和维护,所以我们应该只在父组件中修改传给子组件中的值。

上面的代码中调用了 validateProp() 方法对 props 属性的值进行断定。

export function validateProp (
 key: string,
 propOptions: Object,
 propsData: Object,
 vm?: Component
): any {
 const prop = propOptions[key]
 // 这里的 hasOwn() 方法是对 Object.prototype.hasOwnProperty 的封装(修改了 Object.prototype.hasOwnProperty 执行时的 this 指向)
 // 这样的好处至少有两个
 // 1.调用的时候不需要写一长串代码(比如:obj.hasOwnProperty('key')), 直接调用 hasOwn(obj, key)
 // 2.还有一个好处,hasOwn() 方法的实现,先缓存 Object.prototype.hasOwnProperty 到 hasOwnProperty 变量,然后再调用这个方法。这样做可以避免每次调用时都需要作深度(原型链)查找
 // 标识父组件中有没有传入子组件中定义的某个 props 属性,注意这里取反了,即父组件中没有传入对应的 props 属性时为 true
 const absent = !hasOwn(propsData, key)
 // 从父组件中传入的属性集合中取出对应的属性值,不管父组件中有没有传
 let value = propsData[key]
 // boolean casting
 // 优先判断布尔类型
 const booleanIndex = getTypeIndex(Boolean, prop.type)
 // 如果为布尔类型,或者其实之一为布尔类型
 if (booleanIndex > -1) {
  // 如果父组件中没传,并且定义的 props 属性也没设置默认值
  if (absent && !hasOwn(prop, 'default')) {
   value = false
   // 如果 value 为空(场景为在子组件元素上只添加 props 属性,或者传个值为空)
   // value === hyphenate(key) 在什么场景下会出现?
   // 类型这样 disable="disable" 或者 data-list="dataList"
 } else if (value === '' || value === hyphenate(key)) {
   // only cast empty string / same name to boolean if
   // boolean has higher priority
   // 并且布尔类型写在字符串类型之前,那么 value 也初始为 true
   const stringIndex = getTypeIndex(String, prop.type)
   if (stringIndex < 0 || booleanIndex < stringIndex) {
    value = true
  }
 }
 }
 // check default value
 // 如果此属性在父组件中没有传给子组件
 if (value === undefined) {
  // 那就取 props 中的默认值
  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
}

上面的方法中如果没能从父组件中传入 props 对应的属性,那么就取子组件中定义 props 属性时设置的默认值。通过这个方法 getPropDefaultValue() 获取默认值。

function getPropDefaultValue (vm: ?Component, prop: PropOptions, key: string): any {
 // no default, return undefined
 // 若连默认值都没有设置,则返回 undefined
 if (!hasOwn(prop, 'default')) {
  return undefined
 }
 // 缓存默认值
 const def = prop.default
 // warn against non-factory defaults for Object & Array
 // 这里就是提示如果是对象或者数组,我们的默认值要以函数的方式返回
 if (process.env.NODE_ENV !== 'production' && isObject(def)) {
  warn(
   'Invalid default value for prop "' + key + '": ' +
   'Props with type Object/Array must use a factory function ' +
   'to return the default value.',
   vm
 )
 }
 // the raw prop value was also undefined from previous render,
 // return previous default value to avoid unnecessary watcher trigger
 if (vm && vm.$options.propsData &&
  vm.$options.propsData[key] === undefined &&
  vm._props[key] !== undefined
 ) {
  return vm._props[key]
 }
 // call factory function for non-Function types
 // a value is Function if its prototype is function even across different execution context
 // 如果默认值是一个函数,并且 prop.type 的类型不是函数
 // 为什么要这么设计呢?由于我们有可能会传入函数的类型,而在定义 props 属性的默认值时数组和对象也是通过一个匿名函数来实现初始值的
 // 所以在这里就需要区别这两种情况,如果是函数类型,则直接把这个函数直接返回,如果是数组或者对象则需要执行这个函数,获取函数内部的返回的值,因为数组或者对象是在给定默认值是是通过函数的形式返回的,所以需要执行这个函数能能获取最终结果。
 return typeof def === 'function' && getType(prop.type) !== 'Function'
  ? def.call(vm)
 : def
}

最后 return 后面的语句需要注意一下。


声明

1.原创文章,不经本站同意,不得以任何形式转载,如有不便,请多多包涵!

2.本文永久链接:http://yunkus.com/post/5e8946b29550816a

3.如果觉得本文对你有帮助,或者解决了你的问题,不妨扫一扫右边的二维码打赏支持,你的一分一毫,可能会让世界变得更美好。

微信
扫一扫,赏我
支付宝
扫一扫,赏我