vue3-yuanma/packages/runtime-core/src/componentProps.ts

497 lines
14 KiB
TypeScript
Raw Normal View History

import { toRaw, lock, unlock, shallowReadonly } from '@vue/reactivity'
2019-05-28 18:06:00 +08:00
import {
EMPTY_OBJ,
camelize,
hyphenate,
capitalize,
isString,
isFunction,
isArray,
2019-06-03 13:44:45 +08:00
isObject,
hasOwn,
toRawType,
PatchFlags,
makeMap,
isReservedProp,
EMPTY_ARR,
def
2019-05-28 18:06:00 +08:00
} from '@vue/shared'
import { warn } from './warning'
2019-09-07 00:58:31 +08:00
import { Data, ComponentInternalInstance } from './component'
import { isEmitListener } from './componentEmits'
import { InternalObjectSymbol } from './vnode'
2019-05-28 18:06:00 +08:00
2019-10-08 21:26:09 +08:00
export type ComponentPropsOptions<P = Data> =
| ComponentObjectPropsOptions<P>
| string[]
export type ComponentObjectPropsOptions<P = Data> = {
2019-06-01 00:47:05 +08:00
[K in keyof P]: Prop<P[K]> | null
2019-05-28 18:06:00 +08:00
}
2019-09-07 00:58:31 +08:00
export type Prop<T> = PropOptions<T> | PropType<T>
2019-05-28 18:06:00 +08:00
type DefaultFactory<T> = () => T | null | undefined
2019-05-31 18:07:43 +08:00
interface PropOptions<T = any> {
2019-05-28 18:06:00 +08:00
type?: PropType<T> | true | null
required?: boolean
default?: T | DefaultFactory<T> | null | undefined
2019-10-22 23:26:48 +08:00
validator?(value: unknown): boolean
2019-05-31 18:07:43 +08:00
}
2019-06-01 00:47:05 +08:00
export type PropType<T> = PropConstructor<T> | PropConstructor<T>[]
type PropConstructor<T = any> =
| { new (...args: any[]): T & object }
| { (): T }
| PropMethod<T>
type PropMethod<T> = T extends (...args: any) => any // if is function with args
? { new (): T; (): T; readonly proptotype: Function } // Create Function like contructor
: never
2019-06-01 00:47:05 +08:00
2019-10-05 22:48:54 +08:00
type RequiredKeys<T, MakeDefaultRequired> = {
[K in keyof T]: T[K] extends
| { required: true }
2019-10-05 22:48:54 +08:00
| (MakeDefaultRequired extends true ? { default: any } : never)
? K
: never
2019-06-01 00:47:05 +08:00
}[keyof T]
2019-10-05 22:48:54 +08:00
type OptionalKeys<T, MakeDefaultRequired> = Exclude<
keyof T,
2019-10-05 22:48:54 +08:00
RequiredKeys<T, MakeDefaultRequired>
>
2019-06-01 00:47:05 +08:00
type InferPropType<T> = T extends null
2019-06-01 17:43:41 +08:00
? any // null & true would fail to infer
: T extends { type: null | true }
2019-10-22 23:52:29 +08:00
? any // somehow `ObjectConstructor` when inferred from { (): T } becomes `any`
: T extends ObjectConstructor | { type: ObjectConstructor }
? { [key: string]: any }
: T extends Prop<infer V> ? V : T
2019-06-01 00:47:05 +08:00
export type ExtractPropTypes<
O,
2019-10-05 22:48:54 +08:00
MakeDefaultRequired extends boolean = true
> = O extends object
2019-11-10 07:40:25 +08:00
? { [K in RequiredKeys<O, MakeDefaultRequired>]: InferPropType<O[K]> } &
{ [K in OptionalKeys<O, MakeDefaultRequired>]?: InferPropType<O[K]> }
2019-06-01 00:47:05 +08:00
: { [K in string]: any }
2019-05-28 18:06:00 +08:00
const enum BooleanFlags {
2019-11-25 05:00:46 +08:00
shouldCast,
shouldCastTrue
2019-05-28 18:06:00 +08:00
}
2019-06-01 00:47:05 +08:00
type NormalizedProp =
| null
| (PropOptions & {
[BooleanFlags.shouldCast]?: boolean
[BooleanFlags.shouldCastTrue]?: boolean
})
2019-05-28 18:06:00 +08:00
// normalized value is a tuple of the actual normalized options
// and an array of prop keys that need value casting (booleans and defaults)
type NormalizedPropsOptions = [Record<string, NormalizedProp>, string[]]
2019-05-28 18:06:00 +08:00
export function initProps(
2019-09-07 00:58:31 +08:00
instance: ComponentInternalInstance,
rawProps: Data | null,
isStateful: number, // result of bitwise flag comparison
isSSR = false
2019-05-29 11:36:16 +08:00
) {
const props: Data = {}
const attrs: Data = {}
def(attrs, InternalObjectSymbol, true)
setFullProps(instance, rawProps, props, attrs)
const options = instance.type.props
// validation
if (__DEV__ && options && rawProps) {
validateProps(props, options)
2019-05-28 18:06:00 +08:00
}
2019-05-30 23:16:15 +08:00
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
}
2019-05-30 23:16:15 +08:00
export function updateProps(
instance: ComponentInternalInstance,
rawProps: Data | null,
optimized: boolean
) {
2019-08-23 21:38:32 +08:00
// allow mutation of propsProxy (which is readonly by default)
2019-05-30 23:16:15 +08:00
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
let kebabKey: string
for (const key in rawCurrentProps) {
if (
!rawProps ||
(!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) {
props[key] = resolvePropValue(
options,
rawProps || EMPTY_OBJ,
key,
undefined
)
} else {
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
2020-03-19 06:14:51 +08:00
if (rawProps) {
2019-05-29 09:18:45 +08:00
for (const key in rawProps) {
const value = rawProps[key]
// key, ref are reserved and never passed down
if (isReservedProp(key)) {
continue
}
// prop option names are camelized during normalization, so to support
// kebab -> camel conversion here we need to camelize the key.
let camelKey
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[key] = value
2019-05-28 18:06:00 +08:00
}
}
}
if (needCastKeys) {
for (let i = 0; i < needCastKeys.length; i++) {
const key = needCastKeys[i]
props[key] = resolvePropValue(options!, props, key, props[key])
2019-05-28 18:06:00 +08:00
}
}
}
2019-05-29 11:36:16 +08:00
function resolvePropValue(
options: NormalizedPropsOptions[0],
props: Data,
key: string,
value: unknown
) {
const opt = options[key]
if (opt != null) {
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 | undefined
): NormalizedPropsOptions | [] {
2019-05-28 18:06:00 +08:00
if (!raw) {
return EMPTY_ARR as any
2019-05-28 18:06:00 +08:00
}
if ((raw as any)._n) {
return (raw as any)._n
2019-05-28 18:06:00 +08:00
}
const normalized: NormalizedPropsOptions[0] = {}
const needCastKeys: NormalizedPropsOptions[1] = []
2019-05-28 18:06:00 +08:00
if (isArray(raw)) {
for (let i = 0; i < raw.length; i++) {
if (__DEV__ && !isString(raw[i])) {
warn(`props must be strings when using array syntax.`, raw[i])
}
const normalizedKey = camelize(raw[i])
if (validatePropName(normalizedKey)) {
normalized[normalizedKey] = EMPTY_OBJ
2019-05-28 18:06:00 +08:00
}
}
} else {
if (__DEV__ && !isObject(raw)) {
warn(`invalid props options`, raw)
}
for (const key in raw) {
const normalizedKey = camelize(key)
if (validatePropName(normalizedKey)) {
2019-05-28 18:06:00 +08:00
const opt = raw[key]
const prop: NormalizedProp = (normalized[normalizedKey] =
2019-05-28 18:06:00 +08:00
isArray(opt) || isFunction(opt) ? { type: opt } : opt)
2020-03-19 06:14:51 +08:00
if (prop) {
2019-05-28 18:06:00 +08:00
const booleanIndex = getTypeIndex(Boolean, prop.type)
const stringIndex = getTypeIndex(String, prop.type)
2019-06-01 00:47:05 +08:00
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)
}
2019-05-28 18:06:00 +08:00
}
}
}
}
const normalizedEntry: NormalizedPropsOptions = [normalized, needCastKeys]
def(raw, '_n', normalizedEntry)
return normalizedEntry
}
2019-05-28 18:06:00 +08:00
// use function string name to check type constructors
// so that it works across vms / iframes.
function getType(ctor: Prop<any>): string {
const match = ctor && ctor.toString().match(/^\s*function (\w+)/)
return match ? match[1] : ''
}
function isSameType(a: Prop<any>, b: Prop<any>): boolean {
return getType(a) === getType(b)
}
function getTypeIndex(
type: Prop<any>,
expectedTypes: PropType<any> | void | null | true
): number {
if (isArray(expectedTypes)) {
for (let i = 0, len = expectedTypes.length; i < len; i++) {
if (isSameType(expectedTypes[i], type)) {
return i
}
}
} else if (isFunction(expectedTypes)) {
2019-05-28 18:06:00 +08:00
return isSameType(expectedTypes, type) ? 0 : -1
}
return -1
}
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
2019-05-28 18:06:00 +08:00
}
function validateProp(
name: string,
2019-10-22 23:26:48 +08:00
value: unknown,
prop: PropOptions,
2019-05-28 18:06:00 +08:00
isAbsent: boolean
) {
const { type, required, validator } = prop
// required!
if (required && isAbsent) {
warn('Missing required prop: "' + name + '"')
return
}
// missing but optional
if (value == null && !prop.required) {
return
}
// type check
if (type != null && type !== true) {
let isValid = false
const types = isArray(type) ? type : [type]
const expectedTypes = []
// value is valid as long as one of the specified types match
for (let i = 0; i < types.length && !isValid; i++) {
const { valid, expectedType } = assertType(value, types[i])
expectedTypes.push(expectedType || '')
isValid = valid
}
if (!isValid) {
warn(getInvalidTypeMessage(name, value, expectedTypes))
return
}
}
// custom validator
if (validator && !validator(value)) {
warn('Invalid prop: custom validator check failed for prop "' + name + '".')
}
}
const isSimpleType = /*#__PURE__*/ makeMap(
'String,Number,Boolean,Function,Symbol'
)
2019-05-28 18:06:00 +08:00
type AssertionResult = {
valid: boolean
expectedType: string
}
2019-10-22 23:26:48 +08:00
function assertType(value: unknown, type: PropConstructor): AssertionResult {
2019-05-28 18:06:00 +08:00
let valid
const expectedType = getType(type)
if (isSimpleType(expectedType)) {
2019-05-28 18:06:00 +08:00
const t = typeof value
valid = t === expectedType.toLowerCase()
// for primitive wrapper objects
if (!valid && t === 'object') {
valid = value instanceof type
}
} else if (expectedType === 'Object') {
valid = toRawType(value) === 'Object'
} else if (expectedType === 'Array') {
valid = isArray(value)
} else {
valid = value instanceof type
}
return {
valid,
expectedType
}
}
function getInvalidTypeMessage(
name: string,
2019-10-22 23:26:48 +08:00
value: unknown,
2019-05-28 18:06:00 +08:00
expectedTypes: string[]
): string {
let message =
`Invalid prop: type check failed for prop "${name}".` +
` Expected ${expectedTypes.map(capitalize).join(', ')}`
const expectedType = expectedTypes[0]
const receivedType = toRawType(value)
const expectedValue = styleValue(value, expectedType)
const receivedValue = styleValue(value, receivedType)
// check if we need to specify expected value
if (
expectedTypes.length === 1 &&
isExplicable(expectedType) &&
!isBoolean(expectedType, receivedType)
) {
message += ` with value ${expectedValue}`
}
message += `, got ${receivedType} `
// check if we need to specify received value
if (isExplicable(receivedType)) {
message += `with value ${receivedValue}.`
}
return message
}
2019-10-22 23:26:48 +08:00
function styleValue(value: unknown, type: string): string {
2019-05-28 18:06:00 +08:00
if (type === 'String') {
return `"${value}"`
} else if (type === 'Number') {
return `${Number(value)}`
} else {
return `${value}`
}
}
function isExplicable(type: string): boolean {
const explicitTypes = ['string', 'number', 'boolean']
return explicitTypes.some(elem => type.toLowerCase() === elem)
}
function isBoolean(...args: string[]): boolean {
return args.some(elem => elem.toLowerCase() === 'boolean')
}