Vue 3.0 Props的初始化和更新流程的细节分析
Vue.js
可以让组件的使用者在组件外部传递props
参数,组件拿到这些props
的值来实现各种各样的功能。本文我们就来探讨下组件props
的初始化和更新流程。
在前一篇文章中,我们知道setup
函数的第一个参数是props
,本文我们就来了解下props
是如何初始化和更新的。
在开始之前我们先弄清两个概念:
Props配置:就是编写组件时写的
props
属性,描述一个组件的Props的数据类型和默认值等信息。例如组件定义时:props: ['msg']
Props数据:是在使用组件时给组件传递的数据。例如组件使用时:
<HelloWorld msg="你好" />
Props
的初始化流程
normalizePropsOptions
进行Props标准化配置
挂载组件的第一步是调用createComponentInstance
来创建组件实例对象,初始化的过程中就实现了标准化props的配置normalizePropsOptions
方法:
const instance: ComponentInternalInstance = {
// 省略...
propsOptions: normalizePropsOptions(type, appContext),
// 省略...
return instance
}
接下来我们就来看看标准化Props配置normalizePropsOptions
方法的具体逻辑。
export function normalizePropsOptions(
comp: ConcreteComponent,
appContext: AppContext,
asMixin = false
): NormalizedPropsOptions {
// 1.
const cache = appContext.propsCache
const cached = cache.get(comp)
if (cached) {
return cached
}
const raw = comp.props
const normalized: NormalizedPropsOptions[0] = {}
const needCastKeys: NormalizedPropsOptions[1] = []
// 2.
let hasExtends = false
if (__FEATURE_OPTIONS_API__ && !isFunction(comp)) {
const extendProps = (raw: ComponentOptions) => {
if (__COMPAT__ && isFunction(raw)) {
raw = raw.options
}
hasExtends = true
const [props, keys] = normalizePropsOptions(raw, appContext, true)
extend(normalized, props)
if (keys) needCastKeys.push(...keys)
}
if (!asMixin && appContext.mixins.length) {
appContext.mixins.forEach(extendProps)
}
if (comp.extends) {
extendProps(comp.extends)
}
if (comp.mixins) {
comp.mixins.forEach(extendProps)
}
}
if (!raw && !hasExtends) {
cache.set(comp, EMPTY_ARR as any)
return EMPTY_ARR as any
}
// 3
if (isArray(raw)) {
for (let i = 0; i < raw.length; i++) {
const normalizedKey = camelize(raw[I])
if (validatePropName(normalizedKey)) {
normalized[normalizedKey] = EMPTY_OBJ
}
}
} else if (raw) {
// 4
for (const key in raw) {
const normalizedKey = camelize(key)
if (validatePropName(normalizedKey)) {
const opt = raw[key]
const prop: NormalizedProp = (normalized[normalizedKey] =
isArray(opt) || isFunction(opt) ? { type: opt } : opt)
if (prop) {
const booleanIndex = getTypeIndex(Boolean, prop.type)
const stringIndex = getTypeIndex(String, prop.type)
prop[BooleanFlags.shouldCast] = booleanIndex > -1
prop[BooleanFlags.shouldCastTrue] =
stringIndex < 0 || booleanIndex < stringIndex
// if the prop needs boolean casting or default value
if (booleanIndex > -1 || hasOwn(prop, 'default')) {
needCastKeys.push(normalizedKey)
}
}
}
}
}
// 5
const res: NormalizedPropsOptions = [normalized, needCastKeys]
cache.set(comp, res)
return res
}
标准化Props配置的代码解释:
- 先从
appContext.propsCache
中去获取组件对象为key
的配置缓存,如果取到了直接返回缓存结果;- 再处理
extends
和mixins
中的Props属性,他们二者的作用是扩展组件的定义,所以需要递归他们定义的Props执行normalizePropsOptions
方法,然后将结果放在组件的存储结果中。(从处理来看我们知道extends
只能有一个并被优先处理,mixins
可以有多个)- 如果Props配置是数组且每个元素是个字符串,则将字符串改为驼峰命名,并我每个key创建一个空对象.
定义:props: ['age','message-id']
结果:propOptions: {age: {}, messageId: {}}
- 如果Props配置是对象,则标准化每个不是以$开头的
prop
的定义。首先把数组和函数转换成对象:{type: prop}
。然后判断如果prop
的type
属性中有定义Boolean
,则标记为需要转换数据;如果prop
的type
属性中Boolean
存在,String
不存在或者Boolean
在String
前面,此时标记为需要转换成boolean类型。
定义:props: {name: String, intro: [Boolean, String]}
结果:propOptions.normalized:
normalized
propOptions
- 如果
prop
含有default
或者类型包含了Boolean
,则标记为key
的值需要转换,放在needCastKeys
中。
- 将获取到的
propOptions
缓存到appContext.propsCache
中。
initProps
设置Props初始化
我们前面的章节有提到在setupComponent
设置组件对象的时候会调用initProps(instance, props, isStateful, isSSR)
进行Props初始化处理。我们接下来就来介绍下它:
initProps
的代码逻辑如下:
initProps
初始化的过程分为四个步骤:
- 给Props进行设值;
- 没有传值的Props将其值设置为
undefined
;- 如果是开发环境,进行Props的验证,给出错误提示;
- 将
props
变为响应式数据赋值给组件实例对象,将attrs
赋值给组件实例对象。
我们接下来逐步分析。
setFullProps
设置props
的值
setFullProps
为了方便理解,在此给个例子:
demo设置的流程解释:
- 遍历
rawProps
的值,例子中就是{name: "Lan", address: "北京东城", age: "18", intro: ""}
;- 如果
propsOptions
中有rawProps
对应的key
,如果不需要转换,就直接赋值给props
,如果需要转换值,则先暂存到rawCastValues
中;如果propsOptions
中没有rawProps
对应的key
,并且不是事件相关的属性,则将其存到attrs
中。
props = {address: "北京东城"};
attrs = {age: 18};
rawCastValues = {intro: "", name: "Lan"};
- 通过
resolvePropValue
进行转换:
- 如果有
default
且父组件没有传递值的情况下直接使用默认值,否则用传入的值(例子中的name) - 如果
key[0] === true
,如果父组件没有传值且没有默认值就设置为false
, 如果父组件有传值,这种情况下如果key[1] === true
且字符串为空,这设置为true
(例子中的intro),否则为false
。
props = {address: "北京东城",intro: true,name: "Lan"};
attrs = {age: 18};
validateProps
验证props
值的合法性
validateProps
验证合法性主要有如下几个规则:
- 如果必须传值,但是没有传值就报警告
- 如果不必须传值,传值为null, 有效返回
- 如果类型不匹配就报警告
- 如果validator, 传入的值验证不通过报警告
props
设置成浅响应式对象和赋值
instance.props = shallowReactive(props)
instance.props = props
instance.attrs = attrs
这一步很好理解。shallowReactive
表示只监测props
的变化,内部属性的变化不会被监测。
留个小问题:
props
为什么要设置成响应式呢?
至此,prop的初始化就完成了。在后面的执行setup
函数中就能将props
对象传递给子组件了。
Props
更新流程
更新Props
触发的渲染流程
Props
是在父组件定义然后传给子组件的,所以Props
值的变化会父组件的重新渲染。父组件的重新渲染会触发patch
,然后当更新子组件的时候触发updateComponent
流程,由于props
变化了,hasPropsChanged
会返回true,也就是说shouldUpdateComponent
为true
,此时给子组件的next
设置为新的VNode,然后执行子组件的重新渲染。
const updateComponent = (n1: VNode, n2: VNode, optimized: boolean) => {
const instance = (n2.component = n1.component)!
if (shouldUpdateComponent(n1, n2, optimized)) {
// normal update
instance.next = n2
// in case the child component is also queued, remove it to avoid
// double updating the same child component in the same flush.
invalidateJob(instance.update)
// instance.update is the reactive effect.
instance.update()
}
}
export function shouldUpdateComponent(
prevVNode: VNode,
nextVNode: VNode,
optimized?: boolean
): boolean {
const { props: prevProps, children: prevChildren, component } = prevVNode
const { props: nextProps, children: nextChildren, patchFlag } = nextVNode
// 省略...
return hasPropsChanged(prevProps, nextProps, emits)
}
当子组件重新渲染的时候会执行componentUpdateFn
方法,且此时新的VNode的next
有值,触发的
updateComponentPreRender
流程中会调用updateProps
方法更新子组件实例对象的props
。
const updateComponentPreRender = (
instance: ComponentInternalInstance,
nextVNode: VNode,
optimized: boolean
) => {
nextVNode.component = instance
const prevProps = instance.vnode.props
instance.vnode = nextVNode
instance.next = null
// 更新props
updateProps(instance, nextVNode.props, prevProps, optimized)
}
更新props
后再执行对subTree VNode
执行patch
进行更新子组件。
这里我们回过头来看看pros是如何更新的。
updateProps
更新的具体细节
它的主要目标就是把父组件渲染时得到的新值更新到子组件的实例对象的props
中。
在Vue的编译阶段可以知道组件的VNode的PatchFlags
:
- 如果是PatchFlags
是
PatchFlags.PROPS, 则只更新
vnode.dynamicProps`即动态props的值。 - 如果当前组件的
PatchFlags
是PatchFlags.FULL_PROPS
, 这时候先执行setFullProps
流程,然后删除掉不再使用的动态动态props的值。
这里把可略过的代码附上:
export function updateProps(
instance: ComponentInternalInstance,
rawProps: Data | null,
rawPrevProps: Data | null,
optimized: boolean
) {
const {
props,
attrs,
vnode: { patchFlag }
} = instance
const rawCurrentProps = toRaw(props)
const [options] = instance.propsOptions
let hasAttrsChanged = false
if (
// always force full diff in dev
// - #1942 if hmr is enabled with sfc component
// - vite#872 non-sfc component used by sfc component
!(
__DEV__ &&
(instance.type.__hmrId ||
(instance.parent && instance.parent.type.__hmrId))
) &&
(optimized || patchFlag > 0) &&
!(patchFlag & PatchFlags.FULL_PROPS)
) {
if (patchFlag & PatchFlags.PROPS) {
// Compiler-generated props & no keys change, just set the updated
// the props.
const propsToUpdate = instance.vnode.dynamicProps!
for (let i = 0; i < propsToUpdate.length; i++) {
let key = propsToUpdate[I]
// PROPS flag guarantees rawProps to be non-null
const value = rawProps![key]
if (options) {
// attr / props separation was done on init and will be consistent
// in this code path, so just check if attrs have it.
if (hasOwn(attrs, key)) {
if (value !== attrs[key]) {
attrs[key] = value
hasAttrsChanged = true
}
} else {
const camelizedKey = camelize(key)
props[camelizedKey] = resolvePropValue(
options,
rawCurrentProps,
camelizedKey,
value,
instance,
false /* isAbsent */
)
}
} else {
if (__COMPAT__) {
if (isOn(key) && key.endsWith('Native')) {
key = key.slice(0, -6) // remove Native postfix
} else if (shouldSkipAttr(key, instance)) {
continue
}
}
if (value !== attrs[key]) {
attrs[key] = value
hasAttrsChanged = true
}
}
}
}
} else {
// full props update.
if (setFullProps(instance, rawProps, props, attrs)) {
hasAttrsChanged = true
}
// in case of dynamic props, check if we need to delete keys from
// the props object
let kebabKey: string
for (const key in rawCurrentProps) {
if (
!rawProps ||
// for camelCase
(!hasOwn(rawProps, key) &&
// it's possible the original props was passed in as kebab-case
// and converted to camelCase (#955)
((kebabKey = hyphenate(key)) === key || !hasOwn(rawProps, kebabKey)))
) {
if (options) {
if (
rawPrevProps &&
// for camelCase
(rawPrevProps[key] !== undefined ||
// for kebab-case
rawPrevProps[kebabKey!] !== undefined)
) {
props[key] = resolvePropValue(
options,
rawCurrentProps,
key,
undefined,
instance,
true /* isAbsent */
)
}
} else {
delete props[key]
}
}
}
if (attrs !== rawCurrentProps) {
for (const key in attrs) {
if (!rawProps || !hasOwn(rawProps, key)) {
delete attrs[key]
hasAttrsChanged = true
}
}
}
}
}
提出一个问题
在传递动态的数据时,我们会在属性前面加上冒号:
, 譬如下面的:address="address"
, 这个冒号:
的作用是什么?我们进行标准化Props配置时候是对address
做的处理时为什么没有这个冒号:
做处理呢?
<HelloWorld name="Lan" :address="address" age="18" intro="" />