componentProps.ts 18 KB

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