refactor(runtime-core): refactor props resolution
Improve performance in optimized mode + tests
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { toRaw, lock, unlock } from '@vue/reactivity'
|
||||
import { toRaw, lock, unlock, shallowReadonly } from '@vue/reactivity'
|
||||
import {
|
||||
EMPTY_OBJ,
|
||||
camelize,
|
||||
@@ -13,8 +13,7 @@ import {
|
||||
PatchFlags,
|
||||
makeMap,
|
||||
isReservedProp,
|
||||
EMPTY_ARR,
|
||||
ShapeFlags
|
||||
EMPTY_ARR
|
||||
} from '@vue/shared'
|
||||
import { warn } from './warning'
|
||||
import { Data, ComponentInternalInstance } from './component'
|
||||
@@ -95,45 +94,117 @@ type NormalizedProp =
|
||||
// and an array of prop keys that need value casting (booleans and defaults)
|
||||
type NormalizedPropsOptions = [Record<string, NormalizedProp>, string[]]
|
||||
|
||||
// resolve raw VNode data.
|
||||
// - filter out reserved keys (key, ref)
|
||||
// - extract class and style into $attrs (to be merged onto child
|
||||
// component root)
|
||||
// - for the rest:
|
||||
// - if has declared props: put declared ones in `props`, the rest in `attrs`
|
||||
// - else: everything goes in `props`.
|
||||
|
||||
export function resolveProps(
|
||||
export function initProps(
|
||||
instance: ComponentInternalInstance,
|
||||
rawProps: Data | null
|
||||
rawProps: Data | null,
|
||||
isStateful: number, // result of bitwise flag comparison
|
||||
isSSR = false
|
||||
) {
|
||||
const _options = instance.type.props
|
||||
const hasDeclaredProps = !!_options
|
||||
if (!rawProps && !hasDeclaredProps) {
|
||||
instance.props = instance.attrs = EMPTY_OBJ
|
||||
return
|
||||
const props: Data = {}
|
||||
const attrs: Data = {}
|
||||
setFullProps(instance, rawProps, props, attrs)
|
||||
const options = instance.type.props
|
||||
// validation
|
||||
if (__DEV__ && options && rawProps) {
|
||||
validateProps(props, options)
|
||||
}
|
||||
|
||||
const { 0: options, 1: needCastKeys } = normalizePropsOptions(_options)!
|
||||
const emits = instance.type.emits
|
||||
const props: Data = {}
|
||||
let attrs: Data | undefined = undefined
|
||||
|
||||
// update the instance propsProxy (passed to setup()) to trigger potential
|
||||
// changes
|
||||
const propsProxy = instance.propsProxy
|
||||
const setProp = propsProxy
|
||||
? (key: string, val: unknown) => {
|
||||
props[key] = val
|
||||
propsProxy[key] = val
|
||||
}
|
||||
: (key: string, val: unknown) => {
|
||||
props[key] = val
|
||||
}
|
||||
if (isStateful) {
|
||||
// stateful
|
||||
instance.props = isSSR ? props : shallowReadonly(props)
|
||||
} else {
|
||||
if (!options) {
|
||||
// functional w/ optional props, props === attrs
|
||||
instance.props = attrs
|
||||
} else {
|
||||
// functional w/ declared props
|
||||
instance.props = props
|
||||
}
|
||||
}
|
||||
instance.attrs = attrs
|
||||
}
|
||||
|
||||
export function updateProps(
|
||||
instance: ComponentInternalInstance,
|
||||
rawProps: Data | null,
|
||||
optimized: boolean
|
||||
) {
|
||||
// allow mutation of propsProxy (which is readonly by default)
|
||||
unlock()
|
||||
|
||||
const {
|
||||
props,
|
||||
attrs,
|
||||
vnode: { patchFlag }
|
||||
} = instance
|
||||
const rawOptions = instance.type.props
|
||||
const rawCurrentProps = toRaw(props)
|
||||
const { 0: options } = normalizePropsOptions(rawOptions)
|
||||
|
||||
if ((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++) {
|
||||
const 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)) {
|
||||
attrs[key] = value
|
||||
} else {
|
||||
const camelizedKey = camelize(key)
|
||||
props[camelizedKey] = resolvePropValue(
|
||||
options,
|
||||
rawCurrentProps,
|
||||
camelizedKey,
|
||||
value
|
||||
)
|
||||
}
|
||||
} else {
|
||||
attrs[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// full props update.
|
||||
setFullProps(instance, rawProps, props, attrs)
|
||||
// in case of dynamic props, check if we need to delete keys from
|
||||
// the props object
|
||||
for (const key in rawCurrentProps) {
|
||||
if (!rawProps || !hasOwn(rawProps, key)) {
|
||||
delete props[key]
|
||||
}
|
||||
}
|
||||
for (const key in attrs) {
|
||||
if (!rawProps || !hasOwn(rawProps, key)) {
|
||||
delete attrs[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lock readonly
|
||||
lock()
|
||||
|
||||
if (__DEV__ && rawOptions && rawProps) {
|
||||
validateProps(props, rawOptions)
|
||||
}
|
||||
}
|
||||
|
||||
function setFullProps(
|
||||
instance: ComponentInternalInstance,
|
||||
rawProps: Data | null,
|
||||
props: Data,
|
||||
attrs: Data
|
||||
) {
|
||||
const { 0: options, 1: needCastKeys } = normalizePropsOptions(
|
||||
instance.type.props
|
||||
)
|
||||
const emits = instance.type.emits
|
||||
|
||||
if (rawProps) {
|
||||
for (const key in rawProps) {
|
||||
const value = rawProps[key]
|
||||
@@ -144,95 +215,58 @@ export function resolveProps(
|
||||
// prop option names are camelized during normalization, so to support
|
||||
// kebab -> camel conversion here we need to camelize the key.
|
||||
let camelKey
|
||||
if (hasDeclaredProps && hasOwn(options, (camelKey = camelize(key)))) {
|
||||
setProp(camelKey, value)
|
||||
if (options && hasOwn(options, (camelKey = camelize(key)))) {
|
||||
props[camelKey] = value
|
||||
} else if (!emits || !isEmitListener(emits, key)) {
|
||||
// Any non-declared (either as a prop or an emitted event) props are put
|
||||
// into a separate `attrs` object for spreading. Make sure to preserve
|
||||
// original key casing
|
||||
;(attrs || (attrs = {}))[key] = value
|
||||
attrs[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasDeclaredProps) {
|
||||
// set default values & cast booleans
|
||||
if (needCastKeys) {
|
||||
for (let i = 0; i < needCastKeys.length; i++) {
|
||||
const key = needCastKeys[i]
|
||||
let opt = options[key]
|
||||
if (opt == null) continue
|
||||
const hasDefault = hasOwn(opt, 'default')
|
||||
const currentValue = props[key]
|
||||
// default values
|
||||
if (hasDefault && currentValue === undefined) {
|
||||
const defaultValue = opt.default
|
||||
setProp(key, isFunction(defaultValue) ? defaultValue() : defaultValue)
|
||||
}
|
||||
// boolean casting
|
||||
if (opt[BooleanFlags.shouldCast]) {
|
||||
if (!hasOwn(props, key) && !hasDefault) {
|
||||
setProp(key, false)
|
||||
} else if (
|
||||
opt[BooleanFlags.shouldCastTrue] &&
|
||||
(currentValue === '' || currentValue === hyphenate(key))
|
||||
) {
|
||||
setProp(key, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
// validation
|
||||
if (__DEV__ && rawProps) {
|
||||
for (const key in options) {
|
||||
let opt = options[key]
|
||||
if (opt == null) continue
|
||||
validateProp(key, props[key], opt, !hasOwn(props, key))
|
||||
}
|
||||
props[key] = resolvePropValue(options!, props, key, props[key])
|
||||
}
|
||||
}
|
||||
|
||||
// in case of dynamic props, check if we need to delete keys from
|
||||
// the props proxy
|
||||
const { patchFlag } = instance.vnode
|
||||
if (
|
||||
hasDeclaredProps &&
|
||||
propsProxy &&
|
||||
(patchFlag === 0 || patchFlag & PatchFlags.FULL_PROPS)
|
||||
) {
|
||||
const rawInitialProps = toRaw(propsProxy)
|
||||
for (const key in rawInitialProps) {
|
||||
if (!hasOwn(props, key)) {
|
||||
delete propsProxy[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lock readonly
|
||||
lock()
|
||||
|
||||
if (
|
||||
instance.vnode.shapeFlag & ShapeFlags.FUNCTIONAL_COMPONENT &&
|
||||
!hasDeclaredProps
|
||||
) {
|
||||
// functional component with optional props: use attrs as props
|
||||
instance.props = attrs || EMPTY_OBJ
|
||||
} else {
|
||||
instance.props = props
|
||||
}
|
||||
instance.attrs = attrs || EMPTY_OBJ
|
||||
}
|
||||
|
||||
function validatePropName(key: string) {
|
||||
if (key[0] !== '$') {
|
||||
return true
|
||||
} else if (__DEV__) {
|
||||
warn(`Invalid prop name: "${key}" is a reserved property.`)
|
||||
function resolvePropValue(
|
||||
options: NormalizedPropsOptions[0],
|
||||
props: Data,
|
||||
key: string,
|
||||
value: unknown
|
||||
) {
|
||||
let opt = options[key]
|
||||
if (opt == null) {
|
||||
return value
|
||||
}
|
||||
return false
|
||||
const hasDefault = hasOwn(opt, 'default')
|
||||
// default values
|
||||
if (hasDefault && value === undefined) {
|
||||
const defaultValue = opt.default
|
||||
value = isFunction(defaultValue) ? defaultValue() : defaultValue
|
||||
}
|
||||
// boolean casting
|
||||
if (opt[BooleanFlags.shouldCast]) {
|
||||
if (!hasOwn(props, key) && !hasDefault) {
|
||||
value = false
|
||||
} else if (
|
||||
opt[BooleanFlags.shouldCastTrue] &&
|
||||
(value === '' || value === hyphenate(key))
|
||||
) {
|
||||
value = true
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
export function normalizePropsOptions(
|
||||
raw: ComponentPropsOptions | void
|
||||
): NormalizedPropsOptions {
|
||||
raw: ComponentPropsOptions | undefined
|
||||
): NormalizedPropsOptions | [] {
|
||||
if (!raw) {
|
||||
return EMPTY_ARR as any
|
||||
}
|
||||
@@ -307,9 +341,23 @@ function getTypeIndex(
|
||||
return -1
|
||||
}
|
||||
|
||||
type AssertionResult = {
|
||||
valid: boolean
|
||||
expectedType: string
|
||||
function validateProps(props: Data, rawOptions: ComponentPropsOptions) {
|
||||
const rawValues = toRaw(props)
|
||||
const options = normalizePropsOptions(rawOptions)[0]
|
||||
for (const key in options) {
|
||||
let opt = options[key]
|
||||
if (opt == null) continue
|
||||
validateProp(key, rawValues[key], opt, !hasOwn(rawValues, key))
|
||||
}
|
||||
}
|
||||
|
||||
function validatePropName(key: string) {
|
||||
if (key[0] !== '$') {
|
||||
return true
|
||||
} else if (__DEV__) {
|
||||
warn(`Invalid prop name: "${key}" is a reserved property.`)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function validateProp(
|
||||
@@ -354,6 +402,11 @@ const isSimpleType = /*#__PURE__*/ makeMap(
|
||||
'String,Number,Boolean,Function,Symbol'
|
||||
)
|
||||
|
||||
type AssertionResult = {
|
||||
valid: boolean
|
||||
expectedType: string
|
||||
}
|
||||
|
||||
function assertType(value: unknown, type: PropConstructor): AssertionResult {
|
||||
let valid
|
||||
const expectedType = getType(type)
|
||||
|
||||
Reference in New Issue
Block a user