componentProps.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712
  1. import {
  2. toRaw,
  3. shallowReactive,
  4. trigger,
  5. TriggerOpTypes
  6. } from '@vue/reactivity'
  7. import {
  8. EMPTY_OBJ,
  9. camelize,
  10. hyphenate,
  11. capitalize,
  12. isString,
  13. isFunction,
  14. isArray,
  15. isObject,
  16. hasOwn,
  17. toRawType,
  18. PatchFlags,
  19. makeMap,
  20. isReservedProp,
  21. EMPTY_ARR,
  22. def,
  23. extend,
  24. isOn
  25. } from '@vue/shared'
  26. import { warn } from './warning'
  27. import {
  28. Data,
  29. ComponentInternalInstance,
  30. ComponentOptions,
  31. ConcreteComponent,
  32. setCurrentInstance,
  33. unsetCurrentInstance
  34. } from './component'
  35. import { isEmitListener } from './componentEmits'
  36. import { InternalObjectKey } from './vnode'
  37. import { AppContext } from './apiCreateApp'
  38. import { createPropsDefaultThis } from './compat/props'
  39. import { isCompatEnabled, softAssertCompatEnabled } from './compat/compatConfig'
  40. import { DeprecationTypes } from './compat/compatConfig'
  41. import { shouldSkipAttr } from './compat/attrsFallthrough'
  42. export type ComponentPropsOptions<P = Data> =
  43. | ComponentObjectPropsOptions<P>
  44. | string[]
  45. export type ComponentObjectPropsOptions<P = Data> = {
  46. [K in keyof P]: Prop<P[K]> | null
  47. }
  48. export type Prop<T, D = T> = PropOptions<T, D> | PropType<T>
  49. type DefaultFactory<T> = (props: Data) => T | null | undefined
  50. export interface PropOptions<T = any, D = T> {
  51. type?: PropType<T> | true | null
  52. required?: boolean
  53. default?: D | DefaultFactory<D> | null | undefined | object
  54. validator?(value: unknown): boolean
  55. }
  56. export type PropType<T> = PropConstructor<T> | PropConstructor<T>[]
  57. type PropConstructor<T = any> =
  58. | { new (...args: any[]): T & {} }
  59. | { (): T }
  60. | PropMethod<T>
  61. type PropMethod<T, TConstructor = any> = [T] extends [(...args: any) => any] // if is function with args
  62. ? { new (): TConstructor; (): T; readonly prototype: TConstructor } // Create Function like constructor
  63. : never
  64. type RequiredKeys<T> = {
  65. [K in keyof T]: T[K] extends
  66. | { required: true }
  67. | { default: any }
  68. // don't mark Boolean props as undefined
  69. | BooleanConstructor
  70. | { type: BooleanConstructor }
  71. ? T[K] extends { default: undefined | (() => undefined) }
  72. ? never
  73. : K
  74. : never
  75. }[keyof T]
  76. type OptionalKeys<T> = Exclude<keyof T, RequiredKeys<T>>
  77. type DefaultKeys<T> = {
  78. [K in keyof T]: T[K] extends
  79. | { default: any }
  80. // Boolean implicitly defaults to false
  81. | BooleanConstructor
  82. | { type: BooleanConstructor }
  83. ? T[K] extends { type: BooleanConstructor; required: true } // not default if Boolean is marked as required
  84. ? never
  85. : K
  86. : never
  87. }[keyof T]
  88. type InferPropType<T> = [T] extends [null]
  89. ? any // null & true would fail to infer
  90. : [T] extends [{ type: null | true }]
  91. ? 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`
  92. : [T] extends [ObjectConstructor | { type: ObjectConstructor }]
  93. ? Record<string, any>
  94. : [T] extends [BooleanConstructor | { type: BooleanConstructor }]
  95. ? boolean
  96. : [T] extends [DateConstructor | { type: DateConstructor }]
  97. ? Date
  98. : [T] extends [Prop<infer V, infer D>]
  99. ? unknown extends V
  100. ? D
  101. : V
  102. : T
  103. export type ExtractPropTypes<O> = O extends object
  104. ? { [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
  105. { [K in RequiredKeys<O>]: InferPropType<O[K]> } &
  106. { [K in OptionalKeys<O>]?: InferPropType<O[K]> }
  107. : { [K in string]: any }
  108. const enum BooleanFlags {
  109. shouldCast,
  110. shouldCastTrue
  111. }
  112. // extract props which defined with default from prop options
  113. export type ExtractDefaultPropTypes<O> = O extends object
  114. ? { [K in DefaultKeys<O>]: InferPropType<O[K]> }
  115. : {}
  116. type NormalizedProp =
  117. | null
  118. | (PropOptions & {
  119. [BooleanFlags.shouldCast]?: boolean
  120. [BooleanFlags.shouldCastTrue]?: boolean
  121. })
  122. // normalized value is a tuple of the actual normalized options
  123. // and an array of prop keys that need value casting (booleans and defaults)
  124. export type NormalizedProps = Record<string, NormalizedProp>
  125. export type NormalizedPropsOptions = [NormalizedProps, string[]] | []
  126. export function initProps(
  127. instance: ComponentInternalInstance,
  128. rawProps: Data | null,
  129. isStateful: number, // result of bitwise flag comparison
  130. isSSR = false
  131. ) {
  132. const props: Data = {}
  133. const attrs: Data = {}
  134. def(attrs, InternalObjectKey, 1)
  135. instance.propsDefaults = Object.create(null)
  136. setFullProps(instance, rawProps, props, attrs)
  137. // ensure all declared prop keys are present
  138. for (const key in instance.propsOptions[0]) {
  139. if (!(key in props)) {
  140. props[key] = undefined
  141. }
  142. }
  143. // validation
  144. if (__DEV__) {
  145. validateProps(rawProps || {}, props, instance)
  146. }
  147. if (isStateful) {
  148. // stateful
  149. instance.props = isSSR ? props : shallowReactive(props)
  150. } else {
  151. if (!instance.type.props) {
  152. // functional w/ optional props, props === attrs
  153. instance.props = attrs
  154. } else {
  155. // functional w/ declared props
  156. instance.props = props
  157. }
  158. }
  159. instance.attrs = attrs
  160. }
  161. export function updateProps(
  162. instance: ComponentInternalInstance,
  163. rawProps: Data | null,
  164. rawPrevProps: Data | null,
  165. optimized: boolean
  166. ) {
  167. const {
  168. props,
  169. attrs,
  170. vnode: { patchFlag }
  171. } = instance
  172. const rawCurrentProps = toRaw(props)
  173. const [options] = instance.propsOptions
  174. let hasAttrsChanged = false
  175. if (
  176. // always force full diff in dev
  177. // - #1942 if hmr is enabled with sfc component
  178. // - vite#872 non-sfc component used by sfc component
  179. !(
  180. __DEV__ &&
  181. (instance.type.__hmrId ||
  182. (instance.parent && instance.parent.type.__hmrId))
  183. ) &&
  184. (optimized || patchFlag > 0) &&
  185. !(patchFlag & PatchFlags.FULL_PROPS)
  186. ) {
  187. if (patchFlag & PatchFlags.PROPS) {
  188. // Compiler-generated props & no keys change, just set the updated
  189. // the props.
  190. const propsToUpdate = instance.vnode.dynamicProps!
  191. for (let i = 0; i < propsToUpdate.length; i++) {
  192. let key = propsToUpdate[i]
  193. // PROPS flag guarantees rawProps to be non-null
  194. const value = rawProps![key]
  195. if (options) {
  196. // attr / props separation was done on init and will be consistent
  197. // in this code path, so just check if attrs have it.
  198. if (hasOwn(attrs, key)) {
  199. if (value !== attrs[key]) {
  200. attrs[key] = value
  201. hasAttrsChanged = true
  202. }
  203. } else {
  204. const camelizedKey = camelize(key)
  205. props[camelizedKey] = resolvePropValue(
  206. options,
  207. rawCurrentProps,
  208. camelizedKey,
  209. value,
  210. instance,
  211. false /* isAbsent */
  212. )
  213. }
  214. } else {
  215. if (__COMPAT__) {
  216. if (isOn(key) && key.endsWith('Native')) {
  217. key = key.slice(0, -6) // remove Native postfix
  218. } else if (shouldSkipAttr(key, instance)) {
  219. continue
  220. }
  221. }
  222. if (value !== attrs[key]) {
  223. attrs[key] = value
  224. hasAttrsChanged = true
  225. }
  226. }
  227. }
  228. }
  229. } else {
  230. // full props update.
  231. if (setFullProps(instance, rawProps, props, attrs)) {
  232. hasAttrsChanged = true
  233. }
  234. // in case of dynamic props, check if we need to delete keys from
  235. // the props object
  236. let kebabKey: string
  237. for (const key in rawCurrentProps) {
  238. if (
  239. !rawProps ||
  240. // for camelCase
  241. (!hasOwn(rawProps, key) &&
  242. // it's possible the original props was passed in as kebab-case
  243. // and converted to camelCase (#955)
  244. ((kebabKey = hyphenate(key)) === key || !hasOwn(rawProps, kebabKey)))
  245. ) {
  246. if (options) {
  247. if (
  248. rawPrevProps &&
  249. // for camelCase
  250. (rawPrevProps[key] !== undefined ||
  251. // for kebab-case
  252. rawPrevProps[kebabKey!] !== undefined)
  253. ) {
  254. props[key] = resolvePropValue(
  255. options,
  256. rawCurrentProps,
  257. key,
  258. undefined,
  259. instance,
  260. true /* isAbsent */
  261. )
  262. }
  263. } else {
  264. delete props[key]
  265. }
  266. }
  267. }
  268. // in the case of functional component w/o props declaration, props and
  269. // attrs point to the same object so it should already have been updated.
  270. if (attrs !== rawCurrentProps) {
  271. for (const key in attrs) {
  272. if (!rawProps || !hasOwn(rawProps, key)) {
  273. delete attrs[key]
  274. hasAttrsChanged = true
  275. }
  276. }
  277. }
  278. }
  279. // trigger updates for $attrs in case it's used in component slots
  280. if (hasAttrsChanged) {
  281. trigger(instance, TriggerOpTypes.SET, '$attrs')
  282. }
  283. if (__DEV__) {
  284. validateProps(rawProps || {}, props, instance)
  285. }
  286. }
  287. function setFullProps(
  288. instance: ComponentInternalInstance,
  289. rawProps: Data | null,
  290. props: Data,
  291. attrs: Data
  292. ) {
  293. const [options, needCastKeys] = instance.propsOptions
  294. let hasAttrsChanged = false
  295. let rawCastValues: Data | undefined
  296. if (rawProps) {
  297. for (let key in rawProps) {
  298. // key, ref are reserved and never passed down
  299. if (isReservedProp(key)) {
  300. continue
  301. }
  302. if (__COMPAT__) {
  303. if (key.startsWith('onHook:')) {
  304. softAssertCompatEnabled(
  305. DeprecationTypes.INSTANCE_EVENT_HOOKS,
  306. instance,
  307. key.slice(2).toLowerCase()
  308. )
  309. }
  310. if (key === 'inline-template') {
  311. continue
  312. }
  313. }
  314. const value = rawProps[key]
  315. // prop option names are camelized during normalization, so to support
  316. // kebab -> camel conversion here we need to camelize the key.
  317. let camelKey
  318. if (options && hasOwn(options, (camelKey = camelize(key)))) {
  319. if (!needCastKeys || !needCastKeys.includes(camelKey)) {
  320. props[camelKey] = value
  321. } else {
  322. ;(rawCastValues || (rawCastValues = {}))[camelKey] = value
  323. }
  324. } else if (!isEmitListener(instance.emitsOptions, key)) {
  325. // Any non-declared (either as a prop or an emitted event) props are put
  326. // into a separate `attrs` object for spreading. Make sure to preserve
  327. // original key casing
  328. if (__COMPAT__) {
  329. if (isOn(key) && key.endsWith('Native')) {
  330. key = key.slice(0, -6) // remove Native postfix
  331. } else if (shouldSkipAttr(key, instance)) {
  332. continue
  333. }
  334. }
  335. if (value !== attrs[key]) {
  336. attrs[key] = value
  337. hasAttrsChanged = true
  338. }
  339. }
  340. }
  341. }
  342. if (needCastKeys) {
  343. const rawCurrentProps = toRaw(props)
  344. const castValues = rawCastValues || EMPTY_OBJ
  345. for (let i = 0; i < needCastKeys.length; i++) {
  346. const key = needCastKeys[i]
  347. props[key] = resolvePropValue(
  348. options!,
  349. rawCurrentProps,
  350. key,
  351. castValues[key],
  352. instance,
  353. !hasOwn(castValues, key)
  354. )
  355. }
  356. }
  357. return hasAttrsChanged
  358. }
  359. function resolvePropValue(
  360. options: NormalizedProps,
  361. props: Data,
  362. key: string,
  363. value: unknown,
  364. instance: ComponentInternalInstance,
  365. isAbsent: boolean
  366. ) {
  367. const opt = options[key]
  368. if (opt != null) {
  369. const hasDefault = hasOwn(opt, 'default')
  370. // default values
  371. if (hasDefault && value === undefined) {
  372. const defaultValue = opt.default
  373. if (opt.type !== Function && isFunction(defaultValue)) {
  374. const { propsDefaults } = instance
  375. if (key in propsDefaults) {
  376. value = propsDefaults[key]
  377. } else {
  378. setCurrentInstance(instance)
  379. value = propsDefaults[key] = defaultValue.call(
  380. __COMPAT__ &&
  381. isCompatEnabled(DeprecationTypes.PROPS_DEFAULT_THIS, instance)
  382. ? createPropsDefaultThis(instance, props, key)
  383. : null,
  384. props
  385. )
  386. unsetCurrentInstance()
  387. }
  388. } else {
  389. value = defaultValue
  390. }
  391. }
  392. // boolean casting
  393. if (opt[BooleanFlags.shouldCast]) {
  394. if (isAbsent && !hasDefault) {
  395. value = false
  396. } else if (
  397. opt[BooleanFlags.shouldCastTrue] &&
  398. (value === '' || value === hyphenate(key))
  399. ) {
  400. value = true
  401. }
  402. }
  403. }
  404. return value
  405. }
  406. export function normalizePropsOptions(
  407. comp: ConcreteComponent,
  408. appContext: AppContext,
  409. asMixin = false
  410. ): NormalizedPropsOptions {
  411. const cache = appContext.propsCache
  412. const cached = cache.get(comp)
  413. if (cached) {
  414. return cached
  415. }
  416. const raw = comp.props
  417. const normalized: NormalizedPropsOptions[0] = {}
  418. const needCastKeys: NormalizedPropsOptions[1] = []
  419. // apply mixin/extends props
  420. let hasExtends = false
  421. if (__FEATURE_OPTIONS_API__ && !isFunction(comp)) {
  422. const extendProps = (raw: ComponentOptions) => {
  423. if (__COMPAT__ && isFunction(raw)) {
  424. raw = raw.options
  425. }
  426. hasExtends = true
  427. const [props, keys] = normalizePropsOptions(raw, appContext, true)
  428. extend(normalized, props)
  429. if (keys) needCastKeys.push(...keys)
  430. }
  431. if (!asMixin && appContext.mixins.length) {
  432. appContext.mixins.forEach(extendProps)
  433. }
  434. if (comp.extends) {
  435. extendProps(comp.extends)
  436. }
  437. if (comp.mixins) {
  438. comp.mixins.forEach(extendProps)
  439. }
  440. }
  441. if (!raw && !hasExtends) {
  442. cache.set(comp, EMPTY_ARR as any)
  443. return EMPTY_ARR as any
  444. }
  445. if (isArray(raw)) {
  446. for (let i = 0; i < raw.length; i++) {
  447. if (__DEV__ && !isString(raw[i])) {
  448. warn(`props must be strings when using array syntax.`, raw[i])
  449. }
  450. const normalizedKey = camelize(raw[i])
  451. if (validatePropName(normalizedKey)) {
  452. normalized[normalizedKey] = EMPTY_OBJ
  453. }
  454. }
  455. } else if (raw) {
  456. if (__DEV__ && !isObject(raw)) {
  457. warn(`invalid props options`, raw)
  458. }
  459. for (const key in raw) {
  460. const normalizedKey = camelize(key)
  461. if (validatePropName(normalizedKey)) {
  462. const opt = raw[key]
  463. const prop: NormalizedProp = (normalized[normalizedKey] =
  464. isArray(opt) || isFunction(opt) ? { type: opt } : opt)
  465. if (prop) {
  466. const booleanIndex = getTypeIndex(Boolean, prop.type)
  467. const stringIndex = getTypeIndex(String, prop.type)
  468. prop[BooleanFlags.shouldCast] = booleanIndex > -1
  469. prop[BooleanFlags.shouldCastTrue] =
  470. stringIndex < 0 || booleanIndex < stringIndex
  471. // if the prop needs boolean casting or default value
  472. if (booleanIndex > -1 || hasOwn(prop, 'default')) {
  473. needCastKeys.push(normalizedKey)
  474. }
  475. }
  476. }
  477. }
  478. }
  479. const res: NormalizedPropsOptions = [normalized, needCastKeys]
  480. cache.set(comp, res)
  481. return res
  482. }
  483. function validatePropName(key: string) {
  484. if (key[0] !== '$') {
  485. return true
  486. } else if (__DEV__) {
  487. warn(`Invalid prop name: "${key}" is a reserved property.`)
  488. }
  489. return false
  490. }
  491. // use function string name to check type constructors
  492. // so that it works across vms / iframes.
  493. function getType(ctor: Prop<any>): string {
  494. const match = ctor && ctor.toString().match(/^\s*function (\w+)/)
  495. return match ? match[1] : ctor === null ? 'null' : ''
  496. }
  497. function isSameType(a: Prop<any>, b: Prop<any>): boolean {
  498. return getType(a) === getType(b)
  499. }
  500. function getTypeIndex(
  501. type: Prop<any>,
  502. expectedTypes: PropType<any> | void | null | true
  503. ): number {
  504. if (isArray(expectedTypes)) {
  505. return expectedTypes.findIndex(t => isSameType(t, type))
  506. } else if (isFunction(expectedTypes)) {
  507. return isSameType(expectedTypes, type) ? 0 : -1
  508. }
  509. return -1
  510. }
  511. /**
  512. * dev only
  513. */
  514. function validateProps(
  515. rawProps: Data,
  516. props: Data,
  517. instance: ComponentInternalInstance
  518. ) {
  519. const resolvedValues = toRaw(props)
  520. const options = instance.propsOptions[0]
  521. for (const key in options) {
  522. let opt = options[key]
  523. if (opt == null) continue
  524. validateProp(
  525. key,
  526. resolvedValues[key],
  527. opt,
  528. !hasOwn(rawProps, key) && !hasOwn(rawProps, hyphenate(key))
  529. )
  530. }
  531. }
  532. /**
  533. * dev only
  534. */
  535. function validateProp(
  536. name: string,
  537. value: unknown,
  538. prop: PropOptions,
  539. isAbsent: boolean
  540. ) {
  541. const { type, required, validator } = prop
  542. // required!
  543. if (required && isAbsent) {
  544. warn('Missing required prop: "' + name + '"')
  545. return
  546. }
  547. // missing but optional
  548. if (value == null && !prop.required) {
  549. return
  550. }
  551. // type check
  552. if (type != null && type !== true) {
  553. let isValid = false
  554. const types = isArray(type) ? type : [type]
  555. const expectedTypes = []
  556. // value is valid as long as one of the specified types match
  557. for (let i = 0; i < types.length && !isValid; i++) {
  558. const { valid, expectedType } = assertType(value, types[i])
  559. expectedTypes.push(expectedType || '')
  560. isValid = valid
  561. }
  562. if (!isValid) {
  563. warn(getInvalidTypeMessage(name, value, expectedTypes))
  564. return
  565. }
  566. }
  567. // custom validator
  568. if (validator && !validator(value)) {
  569. warn('Invalid prop: custom validator check failed for prop "' + name + '".')
  570. }
  571. }
  572. const isSimpleType = /*#__PURE__*/ makeMap(
  573. 'String,Number,Boolean,Function,Symbol,BigInt'
  574. )
  575. type AssertionResult = {
  576. valid: boolean
  577. expectedType: string
  578. }
  579. /**
  580. * dev only
  581. */
  582. function assertType(value: unknown, type: PropConstructor): AssertionResult {
  583. let valid
  584. const expectedType = getType(type)
  585. if (isSimpleType(expectedType)) {
  586. const t = typeof value
  587. valid = t === expectedType.toLowerCase()
  588. // for primitive wrapper objects
  589. if (!valid && t === 'object') {
  590. valid = value instanceof type
  591. }
  592. } else if (expectedType === 'Object') {
  593. valid = isObject(value)
  594. } else if (expectedType === 'Array') {
  595. valid = isArray(value)
  596. } else if (expectedType === 'null') {
  597. valid = value === null
  598. } else {
  599. valid = value instanceof type
  600. }
  601. return {
  602. valid,
  603. expectedType
  604. }
  605. }
  606. /**
  607. * dev only
  608. */
  609. function getInvalidTypeMessage(
  610. name: string,
  611. value: unknown,
  612. expectedTypes: string[]
  613. ): string {
  614. let message =
  615. `Invalid prop: type check failed for prop "${name}".` +
  616. ` Expected ${expectedTypes.map(capitalize).join(' | ')}`
  617. const expectedType = expectedTypes[0]
  618. const receivedType = toRawType(value)
  619. const expectedValue = styleValue(value, expectedType)
  620. const receivedValue = styleValue(value, receivedType)
  621. // check if we need to specify expected value
  622. if (
  623. expectedTypes.length === 1 &&
  624. isExplicable(expectedType) &&
  625. !isBoolean(expectedType, receivedType)
  626. ) {
  627. message += ` with value ${expectedValue}`
  628. }
  629. message += `, got ${receivedType} `
  630. // check if we need to specify received value
  631. if (isExplicable(receivedType)) {
  632. message += `with value ${receivedValue}.`
  633. }
  634. return message
  635. }
  636. /**
  637. * dev only
  638. */
  639. function styleValue(value: unknown, type: string): string {
  640. if (type === 'String') {
  641. return `"${value}"`
  642. } else if (type === 'Number') {
  643. return `${Number(value)}`
  644. } else {
  645. return `${value}`
  646. }
  647. }
  648. /**
  649. * dev only
  650. */
  651. function isExplicable(type: string): boolean {
  652. const explicitTypes = ['string', 'number', 'boolean']
  653. return explicitTypes.some(elem => type.toLowerCase() === elem)
  654. }
  655. /**
  656. * dev only
  657. */
  658. function isBoolean(...args: string[]): boolean {
  659. return args.some(elem => elem.toLowerCase() === 'boolean')
  660. }