| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718 |
- import {
- toRaw,
- shallowReactive,
- trigger,
- TriggerOpTypes
- } from '@vue/reactivity'
- import {
- EMPTY_OBJ,
- camelize,
- hyphenate,
- capitalize,
- isString,
- isFunction,
- isArray,
- isObject,
- hasOwn,
- toRawType,
- PatchFlags,
- makeMap,
- isReservedProp,
- EMPTY_ARR,
- def,
- extend,
- isOn
- } from '@vue/shared'
- import { warn } from './warning'
- import {
- Data,
- ComponentInternalInstance,
- ComponentOptions,
- ConcreteComponent,
- setCurrentInstance,
- unsetCurrentInstance
- } from './component'
- import { isEmitListener } from './componentEmits'
- import { InternalObjectKey } from './vnode'
- import { AppContext } from './apiCreateApp'
- import { createPropsDefaultThis } from './compat/props'
- import { isCompatEnabled, softAssertCompatEnabled } from './compat/compatConfig'
- import { DeprecationTypes } from './compat/compatConfig'
- import { shouldSkipAttr } from './compat/attrsFallthrough'
- export type ComponentPropsOptions<P = Data> =
- | ComponentObjectPropsOptions<P>
- | string[]
- export type ComponentObjectPropsOptions<P = Data> = {
- [K in keyof P]: Prop<P[K]> | null
- }
- export type Prop<T, D = T> = PropOptions<T, D> | PropType<T>
- type DefaultFactory<T> = (props: Data) => T | null | undefined
- export interface PropOptions<T = any, D = T> {
- type?: PropType<T> | true | null
- required?: boolean
- default?: D | DefaultFactory<D> | null | undefined | object
- validator?(value: unknown): boolean
- }
- export type PropType<T> = PropConstructor<T> | PropConstructor<T>[]
- type PropConstructor<T = any> =
- | { new (...args: any[]): T & {} }
- | { (): T }
- | PropMethod<T>
- type PropMethod<T, TConstructor = any> = [T] extends [
- ((...args: any) => any) | undefined
- ] // if is function with args, allowing non-required functions
- ? { new (): TConstructor; (): T; readonly prototype: TConstructor } // Create Function like constructor
- : never
- type RequiredKeys<T> = {
- [K in keyof T]: T[K] extends
- | { required: true }
- | { default: any }
- // don't mark Boolean props as undefined
- | BooleanConstructor
- | { type: BooleanConstructor }
- ? T[K] extends { default: undefined | (() => undefined) }
- ? never
- : K
- : never
- }[keyof T]
- type OptionalKeys<T> = Exclude<keyof T, RequiredKeys<T>>
- type DefaultKeys<T> = {
- [K in keyof T]: T[K] extends
- | { default: any }
- // Boolean implicitly defaults to false
- | BooleanConstructor
- | { type: BooleanConstructor }
- ? T[K] extends { type: BooleanConstructor; required: true } // not default if Boolean is marked as required
- ? never
- : K
- : never
- }[keyof T]
- type InferPropType<T> = [T] extends [null]
- ? any // null & true would fail to infer
- : [T] extends [{ type: null | true }]
- ? any // As TS issue https://github.com/Microsoft/TypeScript/issues/14829 // somehow `ObjectConstructor` when inferred from { (): T } becomes `any` // `BooleanConstructor` when inferred from PropConstructor(with PropMethod) becomes `Boolean`
- : [T] extends [ObjectConstructor | { type: ObjectConstructor }]
- ? Record<string, any>
- : [T] extends [BooleanConstructor | { type: BooleanConstructor }]
- ? boolean
- : [T] extends [DateConstructor | { type: DateConstructor }]
- ? Date
- : [T] extends [(infer U)[] | { type: (infer U)[] }]
- ? U extends DateConstructor
- ? Date | InferPropType<U>
- : InferPropType<U>
- : [T] extends [Prop<infer V, infer D>]
- ? unknown extends V
- ? D
- : V
- : T
- export type ExtractPropTypes<O> = O extends object
- ? { [K in keyof O]?: unknown } & // This is needed to keep the relation between the option prop and the props, allowing to use ctrl+click to navigate to the prop options. see: #3656
- { [K in RequiredKeys<O>]: InferPropType<O[K]> } &
- { [K in OptionalKeys<O>]?: InferPropType<O[K]> }
- : { [K in string]: any }
- const enum BooleanFlags {
- shouldCast,
- shouldCastTrue
- }
- // extract props which defined with default from prop options
- export type ExtractDefaultPropTypes<O> = O extends object
- ? { [K in DefaultKeys<O>]: InferPropType<O[K]> }
- : {}
- type NormalizedProp =
- | null
- | (PropOptions & {
- [BooleanFlags.shouldCast]?: boolean
- [BooleanFlags.shouldCastTrue]?: boolean
- })
- // normalized value is a tuple of the actual normalized options
- // and an array of prop keys that need value casting (booleans and defaults)
- export type NormalizedProps = Record<string, NormalizedProp>
- export type NormalizedPropsOptions = [NormalizedProps, string[]] | []
- export function initProps(
- instance: ComponentInternalInstance,
- rawProps: Data | null,
- isStateful: number, // result of bitwise flag comparison
- isSSR = false
- ) {
- const props: Data = {}
- const attrs: Data = {}
- def(attrs, InternalObjectKey, 1)
- instance.propsDefaults = Object.create(null)
- setFullProps(instance, rawProps, props, attrs)
- // ensure all declared prop keys are present
- for (const key in instance.propsOptions[0]) {
- if (!(key in props)) {
- props[key] = undefined
- }
- }
- // validation
- if (__DEV__) {
- validateProps(rawProps || {}, props, instance)
- }
- if (isStateful) {
- // stateful
- instance.props = isSSR ? props : shallowReactive(props)
- } else {
- if (!instance.type.props) {
- // 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,
- 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]
- }
- }
- }
- // in the case of functional component w/o props declaration, props and
- // attrs point to the same object so it should already have been updated.
- if (attrs !== rawCurrentProps) {
- for (const key in attrs) {
- if (!rawProps || !hasOwn(rawProps, key)) {
- delete attrs[key]
- hasAttrsChanged = true
- }
- }
- }
- }
- // trigger updates for $attrs in case it's used in component slots
- if (hasAttrsChanged) {
- trigger(instance, TriggerOpTypes.SET, '$attrs')
- }
- if (__DEV__) {
- validateProps(rawProps || {}, props, instance)
- }
- }
- function setFullProps(
- instance: ComponentInternalInstance,
- rawProps: Data | null,
- props: Data,
- attrs: Data
- ) {
- const [options, needCastKeys] = instance.propsOptions
- let hasAttrsChanged = false
- let rawCastValues: Data | undefined
- if (rawProps) {
- for (let key in rawProps) {
- // key, ref are reserved and never passed down
- if (isReservedProp(key)) {
- continue
- }
- if (__COMPAT__) {
- if (key.startsWith('onHook:')) {
- softAssertCompatEnabled(
- DeprecationTypes.INSTANCE_EVENT_HOOKS,
- instance,
- key.slice(2).toLowerCase()
- )
- }
- if (key === 'inline-template') {
- continue
- }
- }
- const value = rawProps[key]
- // 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)))) {
- if (!needCastKeys || !needCastKeys.includes(camelKey)) {
- props[camelKey] = value
- } else {
- ;(rawCastValues || (rawCastValues = {}))[camelKey] = value
- }
- } else if (!isEmitListener(instance.emitsOptions, 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
- 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
- }
- }
- }
- }
- if (needCastKeys) {
- const rawCurrentProps = toRaw(props)
- const castValues = rawCastValues || EMPTY_OBJ
- for (let i = 0; i < needCastKeys.length; i++) {
- const key = needCastKeys[i]
- props[key] = resolvePropValue(
- options!,
- rawCurrentProps,
- key,
- castValues[key],
- instance,
- !hasOwn(castValues, key)
- )
- }
- }
- return hasAttrsChanged
- }
- function resolvePropValue(
- options: NormalizedProps,
- props: Data,
- key: string,
- value: unknown,
- instance: ComponentInternalInstance,
- isAbsent: boolean
- ) {
- const opt = options[key]
- if (opt != null) {
- const hasDefault = hasOwn(opt, 'default')
- // default values
- if (hasDefault && value === undefined) {
- const defaultValue = opt.default
- if (opt.type !== Function && isFunction(defaultValue)) {
- const { propsDefaults } = instance
- if (key in propsDefaults) {
- value = propsDefaults[key]
- } else {
- setCurrentInstance(instance)
- value = propsDefaults[key] = defaultValue.call(
- __COMPAT__ &&
- isCompatEnabled(DeprecationTypes.PROPS_DEFAULT_THIS, instance)
- ? createPropsDefaultThis(instance, props, key)
- : null,
- props
- )
- unsetCurrentInstance()
- }
- } else {
- value = defaultValue
- }
- }
- // boolean casting
- if (opt[BooleanFlags.shouldCast]) {
- if (isAbsent && !hasDefault) {
- value = false
- } else if (
- opt[BooleanFlags.shouldCastTrue] &&
- (value === '' || value === hyphenate(key))
- ) {
- value = true
- }
- }
- }
- return value
- }
- export function normalizePropsOptions(
- comp: ConcreteComponent,
- appContext: AppContext,
- asMixin = false
- ): NormalizedPropsOptions {
- 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] = []
- // apply mixin/extends props
- 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
- }
- 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
- }
- }
- } else if (raw) {
- if (__DEV__ && !isObject(raw)) {
- warn(`invalid props options`, raw)
- }
- 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)
- }
- }
- }
- }
- }
- const res: NormalizedPropsOptions = [normalized, needCastKeys]
- cache.set(comp, res)
- return res
- }
- function validatePropName(key: string) {
- if (key[0] !== '$') {
- return true
- } else if (__DEV__) {
- warn(`Invalid prop name: "${key}" is a reserved property.`)
- }
- return false
- }
- // 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] : ctor === null ? 'null' : ''
- }
- 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)) {
- return expectedTypes.findIndex(t => isSameType(t, type))
- } else if (isFunction(expectedTypes)) {
- return isSameType(expectedTypes, type) ? 0 : -1
- }
- return -1
- }
- /**
- * dev only
- */
- function validateProps(
- rawProps: Data,
- props: Data,
- instance: ComponentInternalInstance
- ) {
- const resolvedValues = toRaw(props)
- const options = instance.propsOptions[0]
- for (const key in options) {
- let opt = options[key]
- if (opt == null) continue
- validateProp(
- key,
- resolvedValues[key],
- opt,
- !hasOwn(rawProps, key) && !hasOwn(rawProps, hyphenate(key))
- )
- }
- }
- /**
- * dev only
- */
- function validateProp(
- name: string,
- value: unknown,
- prop: PropOptions,
- 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,BigInt'
- )
- type AssertionResult = {
- valid: boolean
- expectedType: string
- }
- /**
- * dev only
- */
- function assertType(value: unknown, type: PropConstructor): AssertionResult {
- let valid
- const expectedType = getType(type)
- if (isSimpleType(expectedType)) {
- 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 = isObject(value)
- } else if (expectedType === 'Array') {
- valid = isArray(value)
- } else if (expectedType === 'null') {
- valid = value === null
- } else {
- valid = value instanceof type
- }
- return {
- valid,
- expectedType
- }
- }
- /**
- * dev only
- */
- function getInvalidTypeMessage(
- name: string,
- value: unknown,
- 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
- }
- /**
- * dev only
- */
- function styleValue(value: unknown, type: string): string {
- if (type === 'String') {
- return `"${value}"`
- } else if (type === 'Number') {
- return `${Number(value)}`
- } else {
- return `${value}`
- }
- }
- /**
- * dev only
- */
- function isExplicable(type: string): boolean {
- const explicitTypes = ['string', 'number', 'boolean']
- return explicitTypes.some(elem => type.toLowerCase() === elem)
- }
- /**
- * dev only
- */
- function isBoolean(...args: string[]): boolean {
- return args.some(elem => elem.toLowerCase() === 'boolean')
- }
|