componentProps.ts 15 KB

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