componentProps.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. import {
  2. EMPTY_ARR,
  3. NO,
  4. YES,
  5. camelize,
  6. hasOwn,
  7. isArray,
  8. isFunction,
  9. isString,
  10. } from '@vue/shared'
  11. import type { VaporComponent, VaporComponentInstance } from './component'
  12. import {
  13. type NormalizedPropsOptions,
  14. baseNormalizePropsOptions,
  15. isEmitListener,
  16. popWarningContext,
  17. pushWarningContext,
  18. resolvePropValue,
  19. setCurrentInstance,
  20. validateProps,
  21. warn,
  22. } from '@vue/runtime-dom'
  23. import { ReactiveFlags } from '@vue/reactivity'
  24. import { normalizeEmitsOptions } from './componentEmits'
  25. import { renderEffect } from './renderEffect'
  26. import { pauseTracking, resetTracking } from '@vue/reactivity'
  27. import type { interopKey } from './vdomInterop'
  28. export type RawProps = Record<string, () => unknown> & {
  29. // generated by compiler for :[key]="x" or v-bind="x"
  30. $?: DynamicPropsSource[] & { [interopKey]?: boolean }
  31. }
  32. export type DynamicPropsSource =
  33. | (() => Record<string, unknown>)
  34. | Record<string, () => unknown>
  35. // TODO optimization: maybe convert functions into computeds
  36. export function resolveSource(
  37. source: Record<string, any> | (() => Record<string, any>),
  38. ): Record<string, any> {
  39. return isFunction(source) ? source() : source
  40. }
  41. export function getPropsProxyHandlers(
  42. comp: VaporComponent,
  43. once?: boolean,
  44. ): [
  45. ProxyHandler<VaporComponentInstance> | null,
  46. ProxyHandler<VaporComponentInstance>,
  47. ] {
  48. if (comp.__propsHandlers) {
  49. return comp.__propsHandlers
  50. }
  51. const propsOptions = normalizePropsOptions(comp)[0]
  52. const emitsOptions = normalizeEmitsOptions(comp)
  53. const isProp = (
  54. propsOptions
  55. ? (key: string | symbol) =>
  56. isString(key) && hasOwn(propsOptions, camelize(key))
  57. : NO
  58. ) as (key: string | symbol) => key is string
  59. const isAttr = propsOptions
  60. ? (key: string) =>
  61. key !== '$' && !isProp(key) && !isEmitListener(emitsOptions, key)
  62. : YES
  63. const getProp = (instance: VaporComponentInstance, key: string | symbol) => {
  64. // this enables direct watching of props and prevents `Invalid watch source` DEV warnings.
  65. if (key === ReactiveFlags.IS_REACTIVE) return true
  66. if (!isProp(key)) return
  67. const rawProps = instance.rawProps
  68. const dynamicSources = rawProps.$
  69. if (dynamicSources) {
  70. let i = dynamicSources.length
  71. let source, isDynamic, rawKey
  72. while (i--) {
  73. source = dynamicSources[i]
  74. isDynamic = isFunction(source)
  75. source = isDynamic ? (source as Function)() : source
  76. for (rawKey in source) {
  77. if (camelize(rawKey) === key) {
  78. return resolvePropValue(
  79. propsOptions!,
  80. key,
  81. isDynamic ? source[rawKey] : source[rawKey](),
  82. instance,
  83. resolveDefault,
  84. )
  85. }
  86. }
  87. }
  88. }
  89. for (const rawKey in rawProps) {
  90. if (camelize(rawKey) === key) {
  91. return resolvePropValue(
  92. propsOptions!,
  93. key,
  94. rawProps[rawKey](),
  95. instance,
  96. resolveDefault,
  97. )
  98. }
  99. }
  100. return resolvePropValue(
  101. propsOptions!,
  102. key,
  103. undefined,
  104. instance,
  105. resolveDefault,
  106. true,
  107. )
  108. }
  109. const getPropValue = once
  110. ? (...args: Parameters<typeof getProp>) => {
  111. pauseTracking()
  112. const value = getProp(...args)
  113. resetTracking()
  114. return value
  115. }
  116. : getProp
  117. const propsHandlers = propsOptions
  118. ? ({
  119. get: (target, key) => getPropValue(target, key),
  120. has: (_, key) => isProp(key),
  121. ownKeys: () => Object.keys(propsOptions),
  122. getOwnPropertyDescriptor(target, key) {
  123. if (isProp(key)) {
  124. return {
  125. configurable: true,
  126. enumerable: true,
  127. get: () => getPropValue(target, key),
  128. }
  129. }
  130. },
  131. } satisfies ProxyHandler<VaporComponentInstance>)
  132. : null
  133. if (__DEV__ && propsOptions) {
  134. Object.assign(propsHandlers!, {
  135. set: propsSetDevTrap,
  136. deleteProperty: propsDeleteDevTrap,
  137. })
  138. }
  139. const getAttr = (target: RawProps, key: string) => {
  140. if (!isProp(key) && !isEmitListener(emitsOptions, key)) {
  141. return getAttrFromRawProps(target, key)
  142. }
  143. }
  144. const hasAttr = (target: RawProps, key: string) => {
  145. if (isAttr(key)) {
  146. return hasAttrFromRawProps(target, key)
  147. } else {
  148. return false
  149. }
  150. }
  151. const getAttrValue = once
  152. ? (...args: Parameters<typeof getAttr>) => {
  153. pauseTracking()
  154. const value = getAttr(...args)
  155. resetTracking()
  156. return value
  157. }
  158. : getAttr
  159. const attrsHandlers = {
  160. get: (target, key: string) => getAttrValue(target.rawProps, key),
  161. has: (target, key: string) => hasAttr(target.rawProps, key),
  162. ownKeys: target => getKeysFromRawProps(target.rawProps).filter(isAttr),
  163. getOwnPropertyDescriptor(target, key: string) {
  164. if (hasAttr(target.rawProps, key)) {
  165. return {
  166. configurable: true,
  167. enumerable: true,
  168. get: () => getAttrValue(target.rawProps, key),
  169. }
  170. }
  171. },
  172. } satisfies ProxyHandler<VaporComponentInstance>
  173. if (__DEV__) {
  174. Object.assign(attrsHandlers, {
  175. set: propsSetDevTrap,
  176. deleteProperty: propsDeleteDevTrap,
  177. })
  178. }
  179. return (comp.__propsHandlers = [propsHandlers, attrsHandlers])
  180. }
  181. export function getAttrFromRawProps(rawProps: RawProps, key: string): unknown {
  182. if (key === '$') return
  183. // need special merging behavior for class & style
  184. const merged = key === 'class' || key === 'style' ? ([] as any[]) : undefined
  185. const dynamicSources = rawProps.$
  186. if (dynamicSources) {
  187. let i = dynamicSources.length
  188. let source, isDynamic
  189. while (i--) {
  190. source = dynamicSources[i]
  191. isDynamic = isFunction(source)
  192. source = isDynamic ? (source as Function)() : source
  193. if (source && hasOwn(source, key)) {
  194. const value = isDynamic ? source[key] : source[key]()
  195. if (merged) {
  196. merged.push(value)
  197. } else {
  198. return value
  199. }
  200. }
  201. }
  202. }
  203. if (hasOwn(rawProps, key)) {
  204. if (merged) {
  205. merged.push(rawProps[key]())
  206. } else {
  207. return rawProps[key]()
  208. }
  209. }
  210. if (merged && merged.length) {
  211. return merged
  212. }
  213. }
  214. export function hasAttrFromRawProps(rawProps: RawProps, key: string): boolean {
  215. if (key === '$') return false
  216. const dynamicSources = rawProps.$
  217. if (dynamicSources) {
  218. let i = dynamicSources.length
  219. while (i--) {
  220. const source = resolveSource(dynamicSources[i])
  221. if (source && hasOwn(source, key)) {
  222. return true
  223. }
  224. }
  225. }
  226. return hasOwn(rawProps, key)
  227. }
  228. export function getKeysFromRawProps(rawProps: RawProps): string[] {
  229. const keys: string[] = []
  230. for (const key in rawProps) {
  231. if (key !== '$') keys.push(key)
  232. }
  233. const dynamicSources = rawProps.$
  234. if (dynamicSources) {
  235. let i = dynamicSources.length
  236. let source
  237. while (i--) {
  238. source = resolveSource(dynamicSources[i])
  239. for (const key in source) {
  240. keys.push(key)
  241. }
  242. }
  243. }
  244. return Array.from(new Set(keys))
  245. }
  246. export function normalizePropsOptions(
  247. comp: VaporComponent,
  248. ): NormalizedPropsOptions {
  249. const cached = comp.__propsOptions
  250. if (cached) return cached
  251. const raw = comp.props
  252. if (!raw) return EMPTY_ARR as []
  253. const normalized: NormalizedPropsOptions[0] = {}
  254. const needCastKeys: NormalizedPropsOptions[1] = []
  255. baseNormalizePropsOptions(raw, normalized, needCastKeys)
  256. return (comp.__propsOptions = [normalized, needCastKeys])
  257. }
  258. function resolveDefault(
  259. factory: (props: Record<string, any>) => unknown,
  260. instance: VaporComponentInstance,
  261. ) {
  262. const prev = setCurrentInstance(instance)
  263. const res = factory.call(null, instance.props)
  264. setCurrentInstance(...prev)
  265. return res
  266. }
  267. export function hasFallthroughAttrs(
  268. comp: VaporComponent,
  269. rawProps: RawProps | null | undefined,
  270. ): boolean {
  271. if (rawProps) {
  272. // determine fallthrough
  273. if (rawProps.$ || !comp.props) {
  274. return true
  275. } else {
  276. // check if rawProps contains any keys not declared
  277. const propsOptions = normalizePropsOptions(comp)[0]!
  278. for (const key in rawProps) {
  279. if (!hasOwn(propsOptions, camelize(key))) {
  280. return true
  281. }
  282. }
  283. }
  284. }
  285. return false
  286. }
  287. /**
  288. * dev only
  289. */
  290. export function setupPropsValidation(instance: VaporComponentInstance): void {
  291. const rawProps = instance.rawProps
  292. if (!rawProps) return
  293. renderEffect(() => {
  294. pushWarningContext(instance)
  295. validateProps(
  296. resolveDynamicProps(rawProps),
  297. instance.props,
  298. normalizePropsOptions(instance.type)[0]!,
  299. )
  300. popWarningContext()
  301. }, true /* noLifecycle */)
  302. }
  303. export function resolveDynamicProps(props: RawProps): Record<string, unknown> {
  304. const mergedRawProps: Record<string, any> = {}
  305. for (const key in props) {
  306. if (key !== '$') {
  307. mergedRawProps[key] = props[key]()
  308. }
  309. }
  310. if (props.$) {
  311. for (const source of props.$) {
  312. const isDynamic = isFunction(source)
  313. const resolved = isDynamic ? source() : source
  314. for (const key in resolved) {
  315. const value = isDynamic ? resolved[key] : (resolved[key] as Function)()
  316. if (key === 'class' || key === 'style') {
  317. const existing = mergedRawProps[key]
  318. if (isArray(existing)) {
  319. existing.push(value)
  320. } else {
  321. mergedRawProps[key] = [existing, value]
  322. }
  323. } else {
  324. mergedRawProps[key] = value
  325. }
  326. }
  327. }
  328. }
  329. return mergedRawProps
  330. }
  331. function propsSetDevTrap(_: any, key: string | symbol) {
  332. warn(
  333. `Attempt to mutate prop ${JSON.stringify(key)} failed. Props are readonly.`,
  334. )
  335. return true
  336. }
  337. function propsDeleteDevTrap(_: any, key: string | symbol) {
  338. warn(
  339. `Attempt to delete prop ${JSON.stringify(key)} failed. Props are readonly.`,
  340. )
  341. return true
  342. }
  343. export const rawPropsProxyHandlers: ProxyHandler<RawProps> = {
  344. get: getAttrFromRawProps,
  345. has: hasAttrFromRawProps,
  346. ownKeys: getKeysFromRawProps,
  347. getOwnPropertyDescriptor(target, key: string) {
  348. if (hasAttrFromRawProps(target, key)) {
  349. return {
  350. configurable: true,
  351. enumerable: true,
  352. get: () => getAttrFromRawProps(target, key),
  353. }
  354. }
  355. },
  356. }