likes
comments
collection
share

给Vue组件props声明传值,是用props还是propsData?

作者站长头像
站长
· 阅读数 44

前情提要

前些天遇到个问题。

问:哎,为什么我这里传props 但是子组件却去不掉值呢?

// 代码场景大概就是这样的

// comp file
const comp = {
  render: () => {},
  created() {
    console.log(this.code);
  },
};

const comCtor = Vue.extend(comp);

// parentComp file
const parentComp = {
  components: { comCtor },
  render: createElement =>
    createElement('comCtor', {
      props: {
        code: 'from props',
      },
    }),
};

很明显这里是子组件没有声明props,导致值传不进去。但当时忙着搞其他事情就没仔细看,一想“我平时好像是传的propsData”

答:你不要用props 用propsData。

// 几分钟后...

问:好像还是不行...

答:哎...(意识到不对劲)

答:奥,你子组件没写props。

问:还是不行...

答:????

后面查看vue源码和文档后发现,这个场景确实得传props。如下Vue文档内容。

给Vue组件props声明传值,是用props还是propsData?

哎?? 那我怎么记得,得传propsData? 难道是我记错了?

给Vue组件props声明传值,是用props还是propsData?

噢! 我平时用的是 new compCtor 的场景。

// comp file
const comp = {
  render: () => {},
  props: ['code'],
  created() {
    console.log(this.code);
  },
};
const comCtor = Vue.extend(comp);

// parentComp file
const compInstance = new comCtor({
  el: document.createElement('div'),
  props: {
    code: 'from props',
  },
  propsData: {
    code: 'from propsData',
  },
});
$root.append(compInstance.$el)

那为什么直接new的时候需要从propsData传入,而render函数又需要从props传入呢? 如果我在子组件内直接写propsData呢?

// comp file
const comp = {
  render: () => {},
  props: ['code'],
  propsData: {
    code: 'from propsData'
  },
  created() {
    console.log(this.code);
  },
};
const comCtor = Vue.extend(comp);

名词声明

为了方便区分,下面将propsOptionspropsData构造options初始化optionscreateData这几个名称做下解释说明:

propsOptions

对props的“声明”。

props: {
  code: {
    type: String,
    default: '',
  },
},

propsData

即将给组件声明的propsOptions赋值的数据。

propsData: {
  code: 'from props',
},

构造options

传入Vue.extend的组件构造函数配置。

const ctorOptions = {
  render: () => {},
  props: ['code'],
  created() {
    console.log(this.code);
  },
};
const comCtor = Vue.extend(ctorOptions);

初始化options

new 调用Vue及其子构造函数时,传入的数据。

const initOptions = {
  el: document.createElement('div'),
  props: {
    code: 'from props',
  },
  propsData: {
    code: 'from propsData',
  },
}
// comCtor 是Vue.extend 出来的构造函数
const compInstance = new comCtor(initOptions);

createData

调用Vue的createElement时,传入的第二个参数值。

const createData = {
  props: {
    code: 'from props',
  },
} 
createElement('comCtor', createData),

直接new调用组件

因为Vue与由Vue.extend出来的组件都是一个函数,如下Vue源码片段。

// Vue
function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

// Vue组件
const Sub = function VueComponent (options) {
  this._init(options)
}

_init方法大概是这样的。

Vue.prototype._init = function (options?: Object) {
  // ...do something
  if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    
  // ...do something
  initState(vm)
}

这里因为是我们自己手动new调用了构造函数,所以初始化options_isComponentfalse,所有会走mergeOptions逻辑。

mergeOptions的作用就是将构造函数的options——我们传入的构造options与Vue本身的options做了一些合并后的数据,里面包含了构造options的数据——与我们传入的初始化options做合并。对于我们的示例:

// comp file
const comp = {
  render: () => {},
  props: ['code'],
  created() {
    console.log(this.code);
  },
};
const comCtor = Vue.extend(comp);

// parentComp file
const compInstance = new comCtor({
  el: document.createElement('div'),
  propsData: {
    code: 'from propsData',
  },
});

其结果大概是这样的:

给Vue组件props声明传值,是用props还是propsData?

这里的propsData就是我们初始化options里传入的propsData,而props就是构造options中的props字段的值。现在都被赋值到了vm.$options中。

回到_init方法,里面会调用initState(vm)这里就会处理vm实例的DataMethodsProps,我们来看下对Props的处理。

export function initState (vm: Component) {
  // ...do something
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  // ...do something
}

function initProps (vm: Component, propsOptions: Object) {
  // 拿到$options的propsData
  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  
  // ...do something
  
  // 这里的toggleObserving 表示对于非根组件
  // 不要把props的对象值处理成响应式的,可以留意下这里先按下不表。
  const isRoot = !vm.$parent
  if (!isRoot) {
    toggleObserving(false)
  }
  
  for (const key in propsOptions) {
    // 按propsOptions的配置,从propsData中取得合适的值
    const value = validateProp(key, propsOptions, propsData, vm)
    
    // ...do something
    
    // 处理响应式
    defineReactive(props, key, value, () => {
      if (!isRoot && !isUpdatingChildComponent) {
        warn(
          // ...一些警告语
          vm
        )
      }
    })
    
  // ...do something
  }
  toggleObserving(true)
}

可以看到,这里对Props的处理就是根据propsOptions的声明,从propsData上面取值。

所以对于我们手动new调用Vue组件构造函数的场景,给propsOptions传值需要用初始化optionspropsData字段。

createElememt 初始化的组件

那么对于createElement调用的组件(即子组件),为何需要用props字段呢?

render: createElement =>
  createElement('comCtor', {
    props: {
      code: 'from props',
    },
}),

简单来说就是,子组件实例被创建的时候,其propsData是在子组件的构造函数中取的,而不像我们手动new调用Vue组件构建函数一样,通过合并构造Options初始化Options得到。

而子组件的构造函数的生成,则是在父组件的render过程,即父组件的render方法被调用时,createElement方法内。Vue会将createData中的props字段值处理成propsData,期间会使用构造Options中的propsOptions校验其合法性。

具体流程我们在下方分析:

前文提到,组件本身也是一个函数:

// Vue组件
const Sub = function VueComponent (options) {
  this._init(options)
}

而new调用组件构造函数,生成子组件的时机,是父组件的patch方法;此时,父组件正通过patch方法将render阶段生成的vnode转换成node并挂载在页面上。

这里是处理流程大致是:

  • patch方法调用createElm方法去生成dom。
  • createElm方法遇到组件的vnode,则调用createComponent
  • createComponent方法调用组件在创建组件vnode时,写入组件data属性的init钩子。
  • init钩子通过createComponentInstanceForVnode方法new调用组件构造函数,生成组件实例。
const componentVNodeHooks = {
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      // kept-alive components, treat as a patch
      const mountedNode: any = vnode // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode)
    } else {
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },
}

export function createComponentInstanceForVnode (
  // we know it's MountedComponentVNode but flow doesn't
  vnode: any,
  // activeInstance in lifecycle state
  parent: any
): Component {
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode,
    parent
  }
  // check inline-template render functions
  const inlineTemplate = vnode.data.inlineTemplate
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render
    options.staticRenderFns = inlineTemplate.staticRenderFns
  }
  return new vnode.componentOptions.Ctor(options)
}

注意,这里new调用子组件构造函数传入的初始化options_isComponent字段值为true

所以接下来的_init方法中,组件的逻辑会走initInternalComponent方法,而不是像我们直接new调用构造函数那样走mergeOptions逻辑。

Vue.prototype._init = function (options?: Object) {

  // do something...
    
  // merge options
  if (options && options._isComponent) {
    // optimize internal component instantiation
    // since dynamic options merging is pretty slow, and none of the
    // internal component options needs special treatment.
    initInternalComponent(vm, options)
  } else {
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    )
  }
  
  // do something...
  
}

我们来看下initInternalComponent方法。

export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  const opts = vm.$options = Object.create(vm.constructor.options)
  // doing this because it's faster than dynamic enumeration.
  const parentVnode = options._parentVnode
  opts.parent = options.parent
  opts._parentVnode = parentVnode

  const vnodeComponentOptions = parentVnode.componentOptions
  
  opts.propsData = vnodeComponentOptions.propsData
  
  opts._parentListeners = vnodeComponentOptions.listeners
  opts._renderChildren = vnodeComponentOptions.children
  opts._componentTag = vnodeComponentOptions.tag

  if (options.render) {
    opts.render = options.render
    opts.staticRenderFns = options.staticRenderFns
  }
}

这里关键的逻辑就是opts.propsData = vnodeComponentOptions.propsData。对于子组件,他的propsData是在构造函数的options中取的。

而子组件的构造函数,是在父组件的render的过程中生成的。

父组件通过render方法是生成vnode的;其中子组件的构造函数就是在生成子组件vnode时创建的,即上面示例的createElement('comCtor', createData)时。

const createData = {
  props: {
    code: 'from props',
  },
}
const parentComp = {
  components: { comCtor },
  render: createElement =>
    createElement('comCtor', createData),
};

createElement最终会返回一个vnode,而对于组件,则会调用createComponent方法处理。

注意:这里的render过程的createElementcreateComponent与上文提到的在patch过程的createElmcreateComponent是完全不同的方法,只是名称有些相近,这里注意区分。

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {

  // do something...
  
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
    
      // do something...
      
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
    
      // do something...
      
    }
  } else {
  
    // do something...
    
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    // do something...
    return vnode
  } else {
    return createEmptyVNode()
  }
}

这里传入_createElementcreateComponent方法中的data值,就是我们在render函数中,调用createElement传入的createData。即:

const createData = {
  props: {
    code: 'from props',
  },
}

我们来看下createComponent方法。

export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  if (isUndef(Ctor)) {
    return
  }
  // 这里基本指向Vue
  const baseCtor = context.$options._base

  // plain options object: turn it into a constructor
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }

  // do something...
  
  data = data || {}

  // extract props
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)

  // do something...

  installComponentHooks(data)

  // return a placeholder vnode
  const name = Ctor.options.name || tag
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )
  
  // do something...
  
  return vnode
}

这里可以注意下installComponentHooks方法,上文我们提到父组件的patch过程中,调用的子组件的init钩子,就是在这个方法里面加载进来的。

可以看到我们要找的子组件构造函数options中的propsData,就是通过extractPropsFromVNodeData,方法创建的。我们来看下这个方法。

export function extractPropsFromVNodeData (
  data: VNodeData,
  Ctor: Class<Component>,
  tag?: string
): ?Object {
  // do something...
  const propOptions = Ctor.options.props
  if (isUndef(propOptions)) {
    return
  }
  
  const res = {}
  
  const { attrs, props } = data
  if (isDef(attrs) || isDef(props)) {
    for (const key in propOptions) {
      const altKey = hyphenate(key)
      
      // do something...
      
      checkProp(res, props, key, altKey, true) ||
      checkProp(res, attrs, key, altKey, false)
    }
  }
  return res
}

function checkProp (
  res: Object,
  hash: ?Object,
  key: string,
  altKey: string,
  preserve: boolean
): boolean {
  if (isDef(hash)) {
    if (hasOwn(hash, key)) {
      res[key] = hash[key]
      if (!preserve) {
        delete hash[key]
      }
      return true
    } else if (hasOwn(hash, altKey)) {
      res[key] = hash[altKey]
      if (!preserve) {
        delete hash[altKey]
      }
      return true
    }
  }
  return false
}

这里可以看到构造Options中的props字段被当成了props的声明即propsOptions;createData中的props字段被当成了props的值,即propsData

响应式

那么如果我们在根组件的propsData中、在子组件的props中,传入一个非响应式的对象,哪个会被处理成响应式的对象?还是两者皆会?或两者皆非?

我们先来看下上文initProps代码中提到的一段需要留意的逻辑:

function initProps (vm: Component, propsOptions: Object) {
  // 拿到$options的propsData
  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  
  // ...do something
  
  // 这里的toggleObserving 表示对于非根组件
  // 不要把props的对象值处理成响应式的,可以留意下这里先按下不表。
  const isRoot = !vm.$parent
  if (!isRoot) {
    toggleObserving(false)
  }
  
  for (const key in propsOptions) {
    // 按propsOptions的配置,从propsData中取得合适的值
    const value = validateProp(key, propsOptions, propsData, vm)
    
    // ...do something
    
    // 处理响应式
    defineReactive(props, key, value, () => {
      if (!isRoot && !isUpdatingChildComponent) {
        warn(
          // ...一些警告语
          vm
        )
      }
    })
    
  // ...do something
  }
  toggleObserving(true)
}

这里有一段判断逻辑,对于isRootfalse的情况,Vue会调用toggleObserving(false)去取消Observing。也就是说,如果propsData的值不是响应式的,则在该过程也不会被出来成响应式的。

而对于isRoot,在我们直接new一个Vue组件构造函数时,组件被当成了根组件,值为true;而当我们的组件被放在模板(template)或render函数中(createElement)时,值则为false

所以对于直接new调用组件构造函数的情况,即使propsData传入的是一个非响应式的对象,Vue仍旧会去深度遍历对象各值,将其处理为一个响应式对象。

而如果组件被放在rendertemplate中,如果传入的props值并非一个响应式的对象,那么Vue是不会去深度遍历将其处理成一个响应式的对象的。

总结

对于手动new调用的Vue组件,我们若想对propsOptions赋值,则需要在new调用的时候,使用构造options里的propsData字段;且即使传入的值不是一个响应式的对象,Vue也会深度遍历,将其处理成一个响应式的对象。

对于render函数中通过createElememt初始化的组件,我们若想对propsOptions赋值,则需要使用createData里的props字段,且该字段值若不是一个响应式对象,Vue也不会将其处理成响应式的。

转载自:https://juejin.cn/post/7238869983623053368
评论
请登录