componentProps.ts 8.9 KB

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