component.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. import {
  2. type ComponentInternalOptions,
  3. type ComponentPropsOptions,
  4. EffectScope,
  5. type EmitFn,
  6. type EmitsOptions,
  7. ErrorCodes,
  8. type GenericAppContext,
  9. type GenericComponentInstance,
  10. type LifecycleHook,
  11. type NormalizedPropsOptions,
  12. type ObjectEmitsOptions,
  13. type SuspenseBoundary,
  14. callWithErrorHandling,
  15. currentInstance,
  16. endMeasure,
  17. expose,
  18. nextUid,
  19. popWarningContext,
  20. pushWarningContext,
  21. queuePostFlushCb,
  22. registerHMR,
  23. simpleSetCurrentInstance,
  24. startMeasure,
  25. unregisterHMR,
  26. warn,
  27. } from '@vue/runtime-dom'
  28. import { type Block, insert, isBlock, remove } from './block'
  29. import {
  30. type ShallowRef,
  31. markRaw,
  32. onScopeDispose,
  33. pauseTracking,
  34. proxyRefs,
  35. resetTracking,
  36. unref,
  37. } from '@vue/reactivity'
  38. import { EMPTY_OBJ, invokeArrayFns, isFunction, isString } from '@vue/shared'
  39. import {
  40. type DynamicPropsSource,
  41. type RawProps,
  42. getKeysFromRawProps,
  43. getPropsProxyHandlers,
  44. hasFallthroughAttrs,
  45. normalizePropsOptions,
  46. resolveDynamicProps,
  47. setupPropsValidation,
  48. } from './componentProps'
  49. import { renderEffect } from './renderEffect'
  50. import { emit, normalizeEmitsOptions } from './componentEmits'
  51. import { setDynamicProps } from './dom/prop'
  52. import {
  53. type DynamicSlotSource,
  54. type RawSlots,
  55. type StaticSlots,
  56. type VaporSlot,
  57. dynamicSlotsProxyHandlers,
  58. getSlot,
  59. } from './componentSlots'
  60. import { hmrReload, hmrRerender } from './hmr'
  61. export { currentInstance } from '@vue/runtime-dom'
  62. export type VaporComponent = FunctionalVaporComponent | ObjectVaporComponent
  63. export type VaporSetupFn = (
  64. props: any,
  65. ctx: Pick<VaporComponentInstance, 'slots' | 'attrs' | 'emit' | 'expose'>,
  66. ) => Block | Record<string, any> | undefined
  67. export type FunctionalVaporComponent = VaporSetupFn &
  68. Omit<ObjectVaporComponent, 'setup'> & {
  69. displayName?: string
  70. } & SharedInternalOptions
  71. export interface ObjectVaporComponent
  72. extends ComponentInternalOptions,
  73. SharedInternalOptions {
  74. setup?: VaporSetupFn
  75. inheritAttrs?: boolean
  76. props?: ComponentPropsOptions
  77. emits?: EmitsOptions
  78. render?(
  79. ctx: any,
  80. props?: any,
  81. emit?: EmitFn,
  82. attrs?: any,
  83. slots?: Record<string, VaporSlot>,
  84. ): Block
  85. name?: string
  86. vapor?: boolean
  87. }
  88. interface SharedInternalOptions {
  89. /**
  90. * Cached normalized props options.
  91. * In vapor mode there are no mixins so normalized options can be cached
  92. * directly on the component
  93. */
  94. __propsOptions?: NormalizedPropsOptions
  95. /**
  96. * Cached normalized props proxy handlers.
  97. */
  98. __propsHandlers?: [ProxyHandler<any> | null, ProxyHandler<any>]
  99. /**
  100. * Cached normalized emits options.
  101. */
  102. __emitsOptions?: ObjectEmitsOptions
  103. }
  104. // In TypeScript, it is actually impossible to have a record type with only
  105. // specific properties that have a different type from the indexed type.
  106. // This makes our rawProps / rawSlots shape difficult to satisfy when calling
  107. // `createComponent` - luckily this is not user-facing, so we don't need to be
  108. // 100% strict. Here we use intentionally wider types to make `createComponent`
  109. // more ergonomic in tests and internal call sites, where we immediately cast
  110. // them into the stricter types.
  111. export type LooseRawProps = Record<
  112. string,
  113. (() => unknown) | DynamicPropsSource[]
  114. > & {
  115. $?: DynamicPropsSource[]
  116. }
  117. export type LooseRawSlots = Record<string, VaporSlot | DynamicSlotSource[]> & {
  118. $?: DynamicSlotSource[]
  119. }
  120. export function createComponent(
  121. component: VaporComponent,
  122. rawProps?: LooseRawProps | null,
  123. rawSlots?: LooseRawSlots | null,
  124. isSingleRoot?: boolean,
  125. appContext: GenericAppContext = (currentInstance &&
  126. currentInstance.appContext) ||
  127. emptyContext,
  128. ): VaporComponentInstance {
  129. // vdom interop enabled and component is not an explicit vapor component
  130. if (appContext.vapor && !component.__vapor) {
  131. return appContext.vapor.vdomMount(component as any, rawProps, rawSlots)
  132. }
  133. if (
  134. isSingleRoot &&
  135. component.inheritAttrs !== false &&
  136. isVaporComponent(currentInstance) &&
  137. currentInstance.hasFallthrough
  138. ) {
  139. // check if we are the single root of the parent
  140. // if yes, inject parent attrs as dynamic props source
  141. const attrs = currentInstance.attrs
  142. if (rawProps) {
  143. ;((rawProps as RawProps).$ || ((rawProps as RawProps).$ = [])).push(
  144. () => attrs,
  145. )
  146. } else {
  147. rawProps = { $: [() => attrs] } as RawProps
  148. }
  149. }
  150. const instance = new VaporComponentInstance(
  151. component,
  152. rawProps as RawProps,
  153. rawSlots as RawSlots,
  154. appContext,
  155. )
  156. if (__DEV__) {
  157. pushWarningContext(instance)
  158. startMeasure(instance, `init`)
  159. // cache normalized options for dev only emit check
  160. instance.propsOptions = normalizePropsOptions(component)
  161. instance.emitsOptions = normalizeEmitsOptions(component)
  162. }
  163. const prev = currentInstance
  164. simpleSetCurrentInstance(instance)
  165. pauseTracking()
  166. if (__DEV__) {
  167. setupPropsValidation(instance)
  168. }
  169. const setupFn = isFunction(component) ? component : component.setup
  170. const setupResult = setupFn
  171. ? callWithErrorHandling(setupFn, instance, ErrorCodes.SETUP_FUNCTION, [
  172. instance.props,
  173. instance,
  174. ]) || EMPTY_OBJ
  175. : EMPTY_OBJ
  176. if (__DEV__ && !isBlock(setupResult)) {
  177. if (isFunction(component)) {
  178. warn(`Functional vapor component must return a block directly.`)
  179. instance.block = []
  180. } else if (!component.render) {
  181. warn(
  182. `Vapor component setup() returned non-block value, and has no render function.`,
  183. )
  184. instance.block = []
  185. } else {
  186. instance.devtoolsRawSetupState = setupResult
  187. // TODO make the proxy warn non-existent property access during dev
  188. instance.setupState = proxyRefs(setupResult)
  189. devRender(instance)
  190. // HMR
  191. if (component.__hmrId) {
  192. registerHMR(instance)
  193. instance.isSingleRoot = isSingleRoot
  194. instance.hmrRerender = hmrRerender.bind(null, instance)
  195. instance.hmrReload = hmrReload.bind(null, instance)
  196. }
  197. }
  198. } else {
  199. // component has a render function but no setup function
  200. // (typically components with only a template and no state)
  201. if (!setupFn && component.render) {
  202. instance.block = callWithErrorHandling(
  203. component.render,
  204. instance,
  205. ErrorCodes.RENDER_FUNCTION,
  206. )
  207. } else {
  208. // in prod result can only be block
  209. instance.block = setupResult as Block
  210. }
  211. }
  212. // single root, inherit attrs
  213. if (
  214. instance.hasFallthrough &&
  215. component.inheritAttrs !== false &&
  216. instance.block instanceof Element &&
  217. Object.keys(instance.attrs).length
  218. ) {
  219. renderEffect(() => {
  220. isApplyingFallthroughProps = true
  221. setDynamicProps(instance.block as Element, [instance.attrs])
  222. isApplyingFallthroughProps = false
  223. })
  224. }
  225. resetTracking()
  226. simpleSetCurrentInstance(prev, instance)
  227. if (__DEV__) {
  228. popWarningContext()
  229. endMeasure(instance, 'init')
  230. }
  231. onScopeDispose(() => unmountComponent(instance), true)
  232. return instance
  233. }
  234. export let isApplyingFallthroughProps = false
  235. /**
  236. * dev only
  237. */
  238. export function devRender(instance: VaporComponentInstance): void {
  239. instance.block =
  240. callWithErrorHandling(
  241. instance.type.render!,
  242. instance,
  243. ErrorCodes.RENDER_FUNCTION,
  244. [
  245. instance.setupState,
  246. instance.props,
  247. instance.emit,
  248. instance.attrs,
  249. instance.slots,
  250. ],
  251. ) || []
  252. }
  253. const emptyContext: GenericAppContext = {
  254. app: null as any,
  255. config: {},
  256. provides: /*@__PURE__*/ Object.create(null),
  257. }
  258. export class VaporComponentInstance implements GenericComponentInstance {
  259. vapor: true
  260. uid: number
  261. type: VaporComponent
  262. root: GenericComponentInstance | null
  263. parent: GenericComponentInstance | null
  264. appContext: GenericAppContext
  265. block: Block
  266. scope: EffectScope
  267. rawProps: RawProps
  268. rawSlots: RawSlots
  269. props: Record<string, any>
  270. attrs: Record<string, any>
  271. propsDefaults: Record<string, any> | null
  272. slots: StaticSlots
  273. // to hold vnode props / slots in vdom interop mode
  274. rawPropsRef?: ShallowRef<any>
  275. rawSlotsRef?: ShallowRef<any>
  276. emit: EmitFn
  277. emitted: Record<string, boolean> | null
  278. expose: (exposed: Record<string, any>) => void
  279. exposed: Record<string, any> | null
  280. exposeProxy: Record<string, any> | null
  281. // for useTemplateRef()
  282. refs: Record<string, any>
  283. // for provide / inject
  284. provides: Record<string, any>
  285. // for useId
  286. ids: [string, number, number]
  287. // for suspense
  288. suspense: SuspenseBoundary | null
  289. hasFallthrough: boolean
  290. // lifecycle hooks
  291. isMounted: boolean
  292. isUnmounted: boolean
  293. isDeactivated: boolean
  294. isUpdating: boolean
  295. bc?: LifecycleHook // LifecycleHooks.BEFORE_CREATE
  296. c?: LifecycleHook // LifecycleHooks.CREATED
  297. bm?: LifecycleHook // LifecycleHooks.BEFORE_MOUNT
  298. m?: LifecycleHook // LifecycleHooks.MOUNTED
  299. bu?: LifecycleHook // LifecycleHooks.BEFORE_UPDATE
  300. u?: LifecycleHook // LifecycleHooks.UPDATED
  301. um?: LifecycleHook // LifecycleHooks.BEFORE_UNMOUNT
  302. bum?: LifecycleHook // LifecycleHooks.UNMOUNTED
  303. da?: LifecycleHook // LifecycleHooks.DEACTIVATED
  304. a?: LifecycleHook // LifecycleHooks.ACTIVATED
  305. rtg?: LifecycleHook // LifecycleHooks.RENDER_TRACKED
  306. rtc?: LifecycleHook // LifecycleHooks.RENDER_TRIGGERED
  307. ec?: LifecycleHook // LifecycleHooks.ERROR_CAPTURED
  308. sp?: LifecycleHook<() => Promise<unknown>> // LifecycleHooks.SERVER_PREFETCH
  309. // dev only
  310. setupState?: Record<string, any>
  311. devtoolsRawSetupState?: any
  312. hmrRerender?: () => void
  313. hmrReload?: (newComp: VaporComponent) => void
  314. propsOptions?: NormalizedPropsOptions
  315. emitsOptions?: ObjectEmitsOptions | null
  316. isSingleRoot?: boolean
  317. constructor(
  318. comp: VaporComponent,
  319. rawProps?: RawProps | null,
  320. rawSlots?: RawSlots | null,
  321. appContext?: GenericAppContext,
  322. ) {
  323. this.vapor = true
  324. this.uid = nextUid()
  325. this.type = comp
  326. this.parent = currentInstance
  327. this.root = currentInstance ? currentInstance.root : this
  328. if (currentInstance) {
  329. this.appContext = currentInstance.appContext
  330. this.provides = currentInstance.provides
  331. this.ids = currentInstance.ids
  332. } else {
  333. this.appContext = appContext || emptyContext
  334. this.provides = Object.create(this.appContext.provides)
  335. this.ids = ['', 0, 0]
  336. }
  337. this.block = null! // to be set
  338. this.scope = new EffectScope(true)
  339. this.emit = emit.bind(null, this)
  340. this.expose = expose.bind(null, this)
  341. this.refs = EMPTY_OBJ
  342. this.emitted =
  343. this.exposed =
  344. this.exposeProxy =
  345. this.propsDefaults =
  346. this.suspense =
  347. null
  348. this.isMounted =
  349. this.isUnmounted =
  350. this.isUpdating =
  351. this.isDeactivated =
  352. false
  353. // init props
  354. this.rawProps = rawProps || EMPTY_OBJ
  355. this.hasFallthrough = hasFallthroughAttrs(comp, rawProps)
  356. if (rawProps || comp.props) {
  357. const [propsHandlers, attrsHandlers] = getPropsProxyHandlers(comp)
  358. this.attrs = new Proxy(this, attrsHandlers)
  359. this.props = comp.props
  360. ? new Proxy(this, propsHandlers!)
  361. : isFunction(comp)
  362. ? this.attrs
  363. : EMPTY_OBJ
  364. } else {
  365. this.props = this.attrs = EMPTY_OBJ
  366. }
  367. // init slots
  368. this.rawSlots = rawSlots || EMPTY_OBJ
  369. this.slots = rawSlots
  370. ? rawSlots.$
  371. ? new Proxy(rawSlots, dynamicSlotsProxyHandlers)
  372. : rawSlots
  373. : EMPTY_OBJ
  374. }
  375. /**
  376. * Expose `getKeysFromRawProps` on the instance so it can be used in code
  377. * paths where it's needed, e.g. `useModel`
  378. */
  379. rawKeys(): string[] {
  380. return getKeysFromRawProps(this.rawProps)
  381. }
  382. }
  383. export function isVaporComponent(
  384. value: unknown,
  385. ): value is VaporComponentInstance {
  386. return value instanceof VaporComponentInstance
  387. }
  388. /**
  389. * Used when a component cannot be resolved at compile time
  390. * and needs rely on runtime resolution - where it might fallback to a plain
  391. * element if the resolution fails.
  392. */
  393. export function createComponentWithFallback(
  394. comp: VaporComponent | string,
  395. rawProps?: LooseRawProps | null,
  396. rawSlots?: LooseRawSlots | null,
  397. isSingleRoot?: boolean,
  398. ): HTMLElement | VaporComponentInstance {
  399. if (!isString(comp)) {
  400. return createComponent(comp, rawProps, rawSlots, isSingleRoot)
  401. }
  402. const el = document.createElement(comp)
  403. // mark single root
  404. ;(el as any).$root = isSingleRoot
  405. if (rawProps) {
  406. renderEffect(() => {
  407. setDynamicProps(el, [resolveDynamicProps(rawProps as RawProps)])
  408. })
  409. }
  410. if (rawSlots) {
  411. if (rawSlots.$) {
  412. // TODO dynamic slot fragment
  413. } else {
  414. insert(getSlot(rawSlots as RawSlots, 'default')!(), el)
  415. }
  416. }
  417. return el
  418. }
  419. export function mountComponent(
  420. instance: VaporComponentInstance,
  421. parent: ParentNode,
  422. anchor?: Node | null | 0,
  423. ): void {
  424. if (__DEV__) {
  425. startMeasure(instance, `mount`)
  426. }
  427. if (instance.bm) invokeArrayFns(instance.bm)
  428. insert(instance.block, parent, anchor)
  429. if (instance.m) queuePostFlushCb(() => invokeArrayFns(instance.m!))
  430. instance.isMounted = true
  431. if (__DEV__) {
  432. endMeasure(instance, `mount`)
  433. }
  434. }
  435. export function unmountComponent(
  436. instance: VaporComponentInstance,
  437. parentNode?: ParentNode,
  438. ): void {
  439. if (instance.isMounted && !instance.isUnmounted) {
  440. if (__DEV__ && instance.type.__hmrId) {
  441. unregisterHMR(instance)
  442. }
  443. if (instance.bum) {
  444. invokeArrayFns(instance.bum)
  445. }
  446. instance.scope.stop()
  447. if (instance.um) {
  448. queuePostFlushCb(() => invokeArrayFns(instance.um!))
  449. }
  450. instance.isUnmounted = true
  451. }
  452. if (parentNode) {
  453. remove(instance.block, parentNode)
  454. }
  455. }
  456. export function getExposed(
  457. instance: GenericComponentInstance,
  458. ): Record<string, any> | undefined {
  459. if (instance.exposed) {
  460. return (
  461. instance.exposeProxy ||
  462. (instance.exposeProxy = new Proxy(markRaw(instance.exposed), {
  463. get: (target, key) => unref(target[key as any]),
  464. }))
  465. )
  466. }
  467. }