component.ts 14 KB

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