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