2
0

component.ts 14 KB

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