component.ts 16 KB

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