|
|
@@ -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]
|
|
|
- }
|
|
|
- }
|
|
|
+function resolvePropValue(
|
|
|
+ options: NormalizedPropsOptions[0],
|
|
|
+ props: Data,
|
|
|
+ key: string,
|
|
|
+ value: unknown
|
|
|
+) {
|
|
|
+ let opt = options[key]
|
|
|
+ if (opt == null) {
|
|
|
+ return value
|
|
|
}
|
|
|
-
|
|
|
- // 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
|
|
|
+ const hasDefault = hasOwn(opt, 'default')
|
|
|
+ // default values
|
|
|
+ if (hasDefault && value === undefined) {
|
|
|
+ const defaultValue = opt.default
|
|
|
+ value = isFunction(defaultValue) ? defaultValue() : defaultValue
|
|
|
}
|
|
|
- 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.`)
|
|
|
+ // 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 false
|
|
|
+ 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)
|