component.ts 15 KB

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