refactor(runtime-core): refactor props resolution

Improve performance in optimized mode + tests
This commit is contained in:
Evan You
2020-04-06 17:37:47 -04:00
parent c28a9196b2
commit ec4a4c1e06
14 changed files with 440 additions and 196 deletions

View File

@@ -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)