vue3-core/packages/runtime-vapor/src/component.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

426 lines
11 KiB
TypeScript
Raw Normal View History

import {
2024-12-04 13:50:54 +08:00
type ComponentInternalOptions,
type ComponentPropsOptions,
2024-12-04 13:50:54 +08:00
EffectScope,
type EmitFn,
type EmitsOptions,
2024-12-04 13:50:54 +08:00
type GenericAppContext,
type GenericComponentInstance,
type LifecycleHook,
type NormalizedPropsOptions,
type ObjectEmitsOptions,
2024-12-06 01:19:20 +08:00
type SuspenseBoundary,
currentInstance,
2024-12-04 13:50:54 +08:00
nextUid,
popWarningContext,
pushWarningContext,
2024-12-08 17:20:34 +08:00
registerHMR,
setCurrentInstance,
warn,
2024-12-04 14:22:26 +08:00
} from '@vue/runtime-dom'
import { type Block, isBlock } from './block'
import { pauseTracking, proxyRefs, resetTracking } from '@vue/reactivity'
2024-12-08 21:22:51 +08:00
import { EMPTY_OBJ, invokeArrayFns, isFunction, isString } from '@vue/shared'
import {
2024-12-04 13:50:54 +08:00
type RawProps,
2024-12-04 23:02:15 +08:00
getPropsProxyHandlers,
2024-12-05 16:14:24 +08:00
hasFallthroughAttrs,
2024-12-04 23:02:15 +08:00
normalizePropsOptions,
2024-12-06 11:10:35 +08:00
resolveDynamicProps,
2024-12-05 16:14:24 +08:00
setupPropsValidation,
2024-12-04 13:50:54 +08:00
} from './componentProps'
import { renderEffect } from './renderEffect'
import { emit, normalizeEmitsOptions } from './componentEmits'
2024-12-06 11:10:35 +08:00
import { setStyle } from './dom/style'
import { setClass, setDynamicProp } from './dom/prop'
import {
type RawSlots,
type Slot,
2024-12-06 22:45:45 +08:00
type StaticSlots,
2024-12-06 23:10:41 +08:00
dynamicSlotsProxyHandlers,
2024-12-07 15:12:32 +08:00
getSlot,
} from './componentSlots'
2024-12-08 21:22:51 +08:00
import { insert, remove } from './dom/node'
import { hmrReload, hmrRerender } from './hmr'
export { currentInstance } from '@vue/runtime-dom'
2024-12-04 13:50:54 +08:00
export type VaporComponent = FunctionalVaporComponent | ObjectVaporComponent
2024-12-02 20:35:45 +08:00
2024-12-04 13:50:54 +08:00
export type VaporSetupFn = (
2024-12-02 09:36:49 +08:00
props: any,
ctx: SetupContext,
2024-12-04 13:50:54 +08:00
) => Block | Record<string, any> | undefined
2024-12-02 20:35:45 +08:00
2024-12-04 13:50:54 +08:00
export type FunctionalVaporComponent = VaporSetupFn &
Omit<ObjectVaporComponent, 'setup'> & {
displayName?: string
2024-12-02 20:35:45 +08:00
} & SharedInternalOptions
2024-12-04 13:50:54 +08:00
export interface ObjectVaporComponent
2024-12-02 20:35:45 +08:00
extends ComponentInternalOptions,
SharedInternalOptions {
2024-12-04 13:50:54 +08:00
setup?: VaporSetupFn
inheritAttrs?: boolean
props?: ComponentPropsOptions
emits?: EmitsOptions
render?(
ctx: any,
props?: any,
emit?: EmitFn,
attrs?: any,
slots?: Record<string, Slot>,
): Block
name?: string
vapor?: boolean
2023-12-06 14:59:11 +08:00
}
2024-12-04 13:50:54 +08:00
interface SharedInternalOptions {
/**
2024-12-04 13:50:54 +08:00
* Cached normalized props options.
* In vapor mode there are no mixins so normalized options can be cached
* directly on the component
*/
2024-12-04 13:50:54 +08:00
__propsOptions?: NormalizedPropsOptions
/**
2024-12-04 13:50:54 +08:00
* Cached normalized props proxy handlers.
*/
2024-12-04 13:50:54 +08:00
__propsHandlers?: [ProxyHandler<any> | null, ProxyHandler<any>]
/**
2024-12-04 13:50:54 +08:00
* Cached normalized emits options.
*/
2024-12-04 13:50:54 +08:00
__emitsOptions?: ObjectEmitsOptions
}
2024-12-04 13:50:54 +08:00
export function createComponent(
component: VaporComponent,
2024-12-07 15:12:32 +08:00
rawProps?: RawProps | null,
rawSlots?: RawSlots | null,
2024-12-04 13:50:54 +08:00
isSingleRoot?: boolean,
): VaporComponentInstance {
// check if we are the single root of the parent
// if yes, inject parent attrs as dynamic props source
if (
isSingleRoot &&
isVaporComponent(currentInstance) &&
currentInstance.hasFallthrough
) {
const attrs = currentInstance.attrs
2024-12-04 13:50:54 +08:00
if (rawProps) {
;(rawProps.$ || (rawProps.$ = [])).push(() => attrs)
2024-12-04 13:50:54 +08:00
} else {
rawProps = { $: [() => attrs] } as RawProps
2024-12-04 13:50:54 +08:00
}
}
2024-12-07 15:12:32 +08:00
const instance = new VaporComponentInstance(component, rawProps, rawSlots)
const resetCurrentInstance = setCurrentInstance(instance)
2024-12-04 13:50:54 +08:00
pauseTracking()
if (__DEV__) {
pushWarningContext(instance)
}
const setupFn = isFunction(component) ? component : component.setup
2024-12-07 21:43:08 +08:00
const setupResult = setupFn
? setupFn(
instance.props,
// @ts-expect-error
setupFn.length > 1 ? new SetupContext(instance) : null,
2024-12-07 21:43:08 +08:00
) || EMPTY_OBJ
: EMPTY_OBJ
if (__DEV__ && !isBlock(setupResult)) {
if (isFunction(component)) {
warn(`Functional vapor component must return a block directly.`)
instance.block = []
} else if (!component.render) {
warn(
`Vapor component setup() returned non-block value, and has no render function.`,
)
instance.block = []
} else {
2024-12-08 17:20:34 +08:00
instance.devtoolsRawSetupState = setupResult
instance.setupState = proxyRefs(setupResult)
devRender(instance)
// HMR
registerHMR(instance)
instance.hmrRerender = hmrRerender.bind(null, instance)
2024-12-08 21:22:51 +08:00
instance.hmrReload = hmrReload.bind(null, instance)
}
} else {
// in prod result can only be block
instance.block = setupResult as Block
}
2024-12-04 13:50:54 +08:00
// single root, inherit attrs
if (
instance.hasFallthrough &&
component.inheritAttrs !== false &&
instance.block instanceof Element &&
Object.keys(instance.attrs).length
) {
renderEffect(() => {
for (const key in instance.attrs) {
setDynamicProp(instance.block as Element, key, instance.attrs[key])
}
})
}
2024-12-04 13:50:54 +08:00
if (__DEV__) {
popWarningContext()
}
resetTracking()
resetCurrentInstance()
2024-12-04 13:50:54 +08:00
return instance
}
2024-12-08 17:20:34 +08:00
/**
* dev only
*/
export function devRender(instance: VaporComponentInstance): void {
instance.block = instance.type.render!.call(
null,
instance.setupState,
instance.props,
instance.emit,
instance.attrs,
instance.slots,
)
}
2024-12-04 13:50:54 +08:00
const emptyContext: GenericAppContext = {
app: null as any,
config: {},
provides: /*@__PURE__*/ Object.create(null),
}
2024-12-04 13:50:54 +08:00
export class VaporComponentInstance implements GenericComponentInstance {
vapor: true
uid: number
2024-12-04 13:50:54 +08:00
type: VaporComponent
parent: GenericComponentInstance | null
2024-12-08 21:22:51 +08:00
children: VaporComponentInstance[] // TODO handle vdom children
2024-12-04 13:50:54 +08:00
appContext: GenericAppContext
2024-12-04 13:50:54 +08:00
block: Block
scope: EffectScope
2024-12-04 13:50:54 +08:00
props: Record<string, any>
attrs: Record<string, any>
2024-12-06 22:45:45 +08:00
slots: StaticSlots
2024-12-04 13:50:54 +08:00
exposed: Record<string, any> | null
2024-12-07 15:12:32 +08:00
rawProps: RawProps
rawSlots: RawSlots
emit: EmitFn
emitted: Record<string, boolean> | null
2024-12-04 13:50:54 +08:00
propsDefaults: Record<string, any> | null
2024-12-04 13:50:54 +08:00
// for useTemplateRef()
refs: Record<string, any>
// for provide / inject
provides: Record<string, any>
2024-12-06 01:19:20 +08:00
// for useId
ids: [string, number, number]
// for suspense
suspense: SuspenseBoundary | null
2024-12-04 13:50:54 +08:00
hasFallthrough: boolean
// lifecycle hooks
isMounted: boolean
isUnmounted: boolean
2024-12-04 13:50:54 +08:00
isDeactivated: boolean
2024-12-06 00:55:00 +08:00
isUpdating: boolean
bc?: LifecycleHook // LifecycleHooks.BEFORE_CREATE
c?: LifecycleHook // LifecycleHooks.CREATED
bm?: LifecycleHook // LifecycleHooks.BEFORE_MOUNT
m?: LifecycleHook // LifecycleHooks.MOUNTED
bu?: LifecycleHook // LifecycleHooks.BEFORE_UPDATE
u?: LifecycleHook // LifecycleHooks.UPDATED
um?: LifecycleHook // LifecycleHooks.BEFORE_UNMOUNT
bum?: LifecycleHook // LifecycleHooks.UNMOUNTED
da?: LifecycleHook // LifecycleHooks.DEACTIVATED
a?: LifecycleHook // LifecycleHooks.ACTIVATED
rtg?: LifecycleHook // LifecycleHooks.RENDER_TRACKED
rtc?: LifecycleHook // LifecycleHooks.RENDER_TRIGGERED
ec?: LifecycleHook // LifecycleHooks.ERROR_CAPTURED
sp?: LifecycleHook<() => Promise<unknown>> // LifecycleHooks.SERVER_PREFETCH
2024-12-04 13:50:54 +08:00
// dev only
setupState?: Record<string, any>
2024-12-08 17:20:34 +08:00
devtoolsRawSetupState?: any
hmrRerender?: () => void
2024-12-08 21:22:51 +08:00
hmrReload?: () => void
2024-12-04 13:50:54 +08:00
propsOptions?: NormalizedPropsOptions
emitsOptions?: ObjectEmitsOptions | null
2024-12-07 15:12:32 +08:00
constructor(
comp: VaporComponent,
rawProps?: RawProps | null,
rawSlots?: RawSlots | null,
) {
this.vapor = true
2024-12-04 13:50:54 +08:00
this.uid = nextUid()
this.type = comp
2024-12-06 01:19:20 +08:00
this.parent = currentInstance // TODO proper parent source when inside vdom instance
2024-12-08 21:22:51 +08:00
this.children = []
if (currentInstance) {
if (isVaporComponent(currentInstance)) {
currentInstance.children.push(this)
}
this.appContext = currentInstance.appContext
this.provides = currentInstance.provides
this.ids = currentInstance.ids
} else {
this.appContext = emptyContext
this.provides = Object.create(this.appContext.provides)
this.ids = ['', 0, 0]
}
2024-12-04 13:50:54 +08:00
this.block = null! // to be set
this.scope = new EffectScope(true)
this.emit = emit.bind(null, this)
this.refs = EMPTY_OBJ
2024-12-06 01:19:20 +08:00
this.emitted = this.exposed = this.propsDefaults = this.suspense = null
2024-12-06 00:55:00 +08:00
this.isMounted =
this.isUnmounted =
this.isUpdating =
this.isDeactivated =
false
2024-12-04 13:50:54 +08:00
// init props
this.rawProps = rawProps || EMPTY_OBJ
2024-12-05 16:14:24 +08:00
this.hasFallthrough = hasFallthroughAttrs(comp, rawProps)
if (rawProps || comp.props) {
const [propsHandlers, attrsHandlers] = getPropsProxyHandlers(comp)
this.props = comp.props ? new Proxy(this, propsHandlers!) : {}
this.attrs = new Proxy(this, attrsHandlers)
} else {
this.props = this.attrs = EMPTY_OBJ
}
// init slots
2024-12-07 15:12:32 +08:00
this.rawSlots = rawSlots || EMPTY_OBJ
2024-12-06 23:10:41 +08:00
this.slots = rawSlots
? rawSlots.$
? new Proxy(rawSlots, dynamicSlotsProxyHandlers)
: rawSlots
: EMPTY_OBJ
2024-12-04 23:02:15 +08:00
if (__DEV__) {
2024-12-05 16:14:24 +08:00
// validate props
if (rawProps) setupPropsValidation(this)
// cache normalized options for dev only emit check
this.propsOptions = normalizePropsOptions(comp)
this.emitsOptions = normalizeEmitsOptions(comp)
}
}
}
2024-12-04 13:50:54 +08:00
export function isVaporComponent(
value: unknown,
): value is VaporComponentInstance {
return value instanceof VaporComponentInstance
}
export class SetupContext {
2024-12-04 13:50:54 +08:00
attrs: Record<string, any>
emit: EmitFn
slots: Readonly<StaticSlots>
2024-12-04 13:50:54 +08:00
expose: (exposed?: Record<string, any>) => void
2024-12-04 13:50:54 +08:00
constructor(instance: VaporComponentInstance) {
this.attrs = instance.attrs
this.emit = instance.emit
this.slots = instance.slots
2024-12-04 13:50:54 +08:00
this.expose = (exposed = {}) => {
instance.exposed = exposed
}
}
}
2024-12-06 11:10:35 +08:00
/**
* Used when a component cannot be resolved at compile time
* and needs rely on runtime resolution - where it might fallback to a plain
* element if the resolution fails.
*/
2024-12-06 11:10:35 +08:00
export function createComponentWithFallback(
comp: VaporComponent | string,
2024-12-07 15:12:32 +08:00
rawProps: RawProps | null | undefined,
rawSlots: RawSlots | null | undefined,
2024-12-06 11:10:35 +08:00
isSingleRoot?: boolean,
): HTMLElement | VaporComponentInstance {
if (!isString(comp)) {
2024-12-07 15:12:32 +08:00
return createComponent(comp, rawProps, rawSlots, isSingleRoot)
2024-12-06 11:10:35 +08:00
}
// eslint-disable-next-line no-restricted-globals
const el = document.createElement(comp)
if (rawProps) {
renderEffect(() => {
let classes: unknown[] | undefined
let styles: unknown[] | undefined
const resolved = resolveDynamicProps(rawProps)
for (const key in resolved) {
const value = resolved[key]
if (key === 'class') (classes ||= []).push(value)
else if (key === 'style') (styles ||= []).push(value)
else setDynamicProp(el, key, value)
}
if (classes) setClass(el, classes)
if (styles) setStyle(el, styles)
})
}
2024-12-07 21:43:08 +08:00
if (rawSlots) {
if (rawSlots.$) {
// TODO dynamic slot fragment
} else {
insert(getSlot(rawSlots, 'default')!(), el)
}
2024-12-07 15:12:32 +08:00
}
2024-12-06 11:10:35 +08:00
return el
}
2024-12-08 21:22:51 +08:00
export function mountComponent(
instance: VaporComponentInstance,
parent: ParentNode,
anchor: Node | null | 0,
): void {
if (!instance.isMounted) {
if (instance.bm) invokeArrayFns(instance.bm)
insert(instance.block, parent, anchor)
// queuePostFlushCb(() => {
if (instance.m) invokeArrayFns(instance.m)
instance.isMounted = true
// })
} else {
insert(instance.block, parent, anchor)
}
}
export function unmountComponent(
instance: VaporComponentInstance,
parent: ParentNode,
): void {
if (instance.isMounted && !instance.isUnmounted) {
if (instance.bum) invokeArrayFns(instance.bum)
// TODO invoke unmount recursively for children
remove(instance.block, parent)
// queuePostFlushCb(() => {
if (instance.um) invokeArrayFns(instance.um)
instance.isUnmounted = true
// })
}
}