componentProps.ts 13 KB

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