给Vue组件props声明传值,是用props还是propsData?
前情提要
前些天遇到个问题。
问:哎,为什么我这里传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文档内容。
哎?? 那我怎么记得,得传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);
名词声明
为了方便区分,下面将propsOptions、propsData、构造options、初始化options、createData这几个名称做下解释说明:
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的_isComponent
为false
,所有会走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',
},
});
其结果大概是这样的:
这里的propsData
就是我们初始化options里传入的propsData
,而props就是构造options中的props
字段的值。现在都被赋值到了vm.$options
中。
回到_init
方法,里面会调用initState(vm)
这里就会处理vm实例的Data
、Methods
和Props
,我们来看下对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传值需要用初始化options的propsData
字段。
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
过程的createElement
和createComponent
与上文提到的在patch
过程的createElm
、createComponent
是完全不同的方法,只是名称有些相近,这里注意区分。
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()
}
}
这里传入_createElement
和createComponent
方法中的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)
}
这里有一段判断逻辑,对于isRoot
为false
的情况,Vue会调用toggleObserving(false)
去取消Observing。也就是说,如果propsData
的值不是响应式的,则在该过程也不会被出来成响应式的。
而对于isRoot
,在我们直接new一个Vue组件构造函数时,组件被当成了根组件,值为true
;而当我们的组件被放在模板(template
)或render
函数中(createElement
)时,值则为false
。
所以对于直接new调用组件构造函数的情况,即使propsData
传入的是一个非响应式的对象,Vue仍旧会去深度遍历对象各值,将其处理为一个响应式对象。
而如果组件被放在render
或template
中,如果传入的props
值并非一个响应式的对象,那么Vue是不会去深度遍历将其处理成一个响应式的对象的。
总结
对于手动new调用的Vue组件,我们若想对propsOptions赋值,则需要在new调用的时候,使用构造options里的propsData
字段;且即使传入的值不是一个响应式的对象,Vue也会深度遍历,将其处理成一个响应式的对象。
对于render
函数中通过createElememt
初始化的组件,我们若想对propsOptions赋值,则需要使用createData里的props
字段,且该字段值若不是一个响应式对象,Vue也不会将其处理成响应式的。
转载自:https://juejin.cn/post/7238869983623053368