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.

528 lines
14 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-09 23:42:23 +08:00
ErrorCodes,
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,
2024-12-09 23:42:23 +08:00
callWithErrorHandling,
currentInstance,
2024-12-10 12:49:47 +08:00
endMeasure,
2024-12-10 17:00:35 +08:00
expose,
2024-12-04 13:50:54 +08:00
nextUid,
popWarningContext,
pushWarningContext,
2024-12-10 18:43:26 +08:00
queuePostFlushCb,
2024-12-08 17:20:34 +08:00
registerHMR,
simpleSetCurrentInstance,
2024-12-10 12:49:47 +08:00
startMeasure,
unregisterHMR,
warn,
2024-12-04 14:22:26 +08:00
} from '@vue/runtime-dom'
import { type Block, insert, isBlock, remove } from './block'
2024-12-10 17:00:35 +08:00
import {
2025-02-02 22:28:35 +08:00
type ShallowRef,
2024-12-10 17:00:35 +08:00
markRaw,
onScopeDispose,
2024-12-10 17:00:35 +08:00
pauseTracking,
proxyRefs,
resetTracking,
2024-12-12 22:34:35 +08:00
unref,
2024-12-10 17:00:35 +08:00
} from '@vue/reactivity'
import { EMPTY_OBJ, invokeArrayFns, isFunction, isString } from '@vue/shared'
import {
type DynamicPropsSource,
2024-12-04 13:50:54 +08:00
type RawProps,
getKeysFromRawProps,
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'
import { setDynamicProps } from './dom/prop'
import {
type DynamicSlotSource,
type RawSlots,
2024-12-06 22:45:45 +08:00
type StaticSlots,
2025-02-03 15:46:40 +08:00
type VaporSlot,
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 { 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,
2024-12-10 17:00:35 +08:00
ctx: Pick<VaporComponentInstance, 'slots' | 'attrs' | 'emit' | 'expose'>,
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,
2025-02-03 15:46:40 +08:00
slots?: Record<string, VaporSlot>,
): 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
}
// In TypeScript, it is actually impossible to have a record type with only
// specific properties that have a different type from the indexed type.
// This makes our rawProps / rawSlots shape difficult to satisfy when calling
// `createComponent` - luckily this is not user-facing, so we don't need to be
// 100% strict. Here we use intentionally wider types to make `createComponent`
// more ergonomic in tests and internal call sites, where we immediately cast
// them into the stricter types.
2024-12-10 21:36:06 +08:00
export type LooseRawProps = Record<
string,
(() => unknown) | DynamicPropsSource[]
> & {
$?: DynamicPropsSource[]
}
2025-02-04 21:38:09 +08:00
export type LooseRawSlots = Record<string, VaporSlot | DynamicSlotSource[]> & {
$?: DynamicSlotSource[]
}
2024-12-04 13:50:54 +08:00
export function createComponent(
component: VaporComponent,
rawProps?: LooseRawProps | null,
rawSlots?: LooseRawSlots | null,
2024-12-04 13:50:54 +08:00
isSingleRoot?: boolean,
2025-02-04 21:38:09 +08:00
appContext: GenericAppContext = (currentInstance &&
currentInstance.appContext) ||
emptyContext,
2024-12-04 13:50:54 +08:00
): VaporComponentInstance {
2025-02-04 21:38:09 +08:00
// vdom interop enabled and component is not an explicit vapor component
2025-02-05 14:16:39 +08:00
if (appContext.vapor && !component.__vapor) {
return appContext.vapor.vdomMount(component as any, rawProps, rawSlots)
2025-02-04 21:38:09 +08:00
}
if (
isSingleRoot &&
2024-12-09 13:12:37 +08:00
component.inheritAttrs !== false &&
isVaporComponent(currentInstance) &&
currentInstance.hasFallthrough
) {
2025-02-04 21:38:09 +08:00
// check if we are the single root of the parent
// if yes, inject parent attrs as dynamic props source
const attrs = currentInstance.attrs
2024-12-04 13:50:54 +08:00
if (rawProps) {
;((rawProps as RawProps).$ || ((rawProps as 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-10 12:49:47 +08:00
const instance = new VaporComponentInstance(
component,
rawProps as RawProps,
rawSlots as RawSlots,
2024-12-10 12:49:47 +08:00
appContext,
)
2024-12-04 13:50:54 +08:00
if (__DEV__) {
pushWarningContext(instance)
2024-12-10 12:49:47 +08:00
startMeasure(instance, `init`)
2025-02-04 21:38:09 +08:00
// cache normalized options for dev only emit check
instance.propsOptions = normalizePropsOptions(component)
instance.emitsOptions = normalizeEmitsOptions(component)
2024-12-04 13:50:54 +08:00
}
2024-12-10 12:49:47 +08:00
const prev = currentInstance
simpleSetCurrentInstance(instance)
pauseTracking()
2025-02-02 22:28:35 +08:00
if (__DEV__) {
setupPropsValidation(instance)
}
2024-12-04 13:50:54 +08:00
const setupFn = isFunction(component) ? component : component.setup
2024-12-07 21:43:08 +08:00
const setupResult = setupFn
2024-12-09 23:42:23 +08:00
? callWithErrorHandling(setupFn, instance, ErrorCodes.SETUP_FUNCTION, [
2024-12-07 21:43:08 +08:00
instance.props,
2024-12-10 17:00:35 +08:00
instance,
2024-12-09 23:42:23 +08:00
]) || EMPTY_OBJ
2024-12-07 21:43:08 +08:00
: 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
2024-12-11 14:33:16 +08:00
// TODO make the proxy warn non-existent property access during dev
2024-12-08 17:20:34 +08:00
instance.setupState = proxyRefs(setupResult)
devRender(instance)
// HMR
if (component.__hmrId) {
registerHMR(instance)
2024-12-08 23:37:40 +08:00
instance.isSingleRoot = isSingleRoot
instance.hmrRerender = hmrRerender.bind(null, instance)
instance.hmrReload = hmrReload.bind(null, instance)
}
}
} else {
// component has a render function but no setup function
// (typically components with only a template and no state)
if (!setupFn && component.render) {
instance.block = callWithErrorHandling(
component.render,
instance,
ErrorCodes.RENDER_FUNCTION,
)
} 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(() => {
isApplyingFallthroughProps = true
setDynamicProps(instance.block as Element, [instance.attrs])
isApplyingFallthroughProps = false
2024-12-04 13:50:54 +08:00
})
}
2024-12-10 12:49:47 +08:00
resetTracking()
simpleSetCurrentInstance(prev, instance)
2024-12-04 13:50:54 +08:00
if (__DEV__) {
popWarningContext()
2024-12-10 12:49:47 +08:00
endMeasure(instance, 'init')
2024-12-04 13:50:54 +08:00
}
onScopeDispose(() => unmountComponent(instance), true)
2024-12-04 13:50:54 +08:00
return instance
}
export let isApplyingFallthroughProps = false
2024-12-08 17:20:34 +08:00
/**
* dev only
*/
export function devRender(instance: VaporComponentInstance): void {
2024-12-09 23:42:23 +08:00
instance.block =
callWithErrorHandling(
instance.type.render!,
instance,
ErrorCodes.RENDER_FUNCTION,
[
instance.setupState,
instance.props,
instance.emit,
instance.attrs,
instance.slots,
],
) || []
2024-12-08 17:20:34 +08:00
}
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
2025-02-04 21:38:09 +08:00
root: GenericComponentInstance | null
2024-12-04 13:50:54 +08:00
parent: GenericComponentInstance | null
appContext: GenericAppContext
2024-12-04 13:50:54 +08:00
block: Block
scope: EffectScope
2024-12-07 15:12:32 +08:00
rawProps: RawProps
rawSlots: RawSlots
2024-12-10 17:00:35 +08:00
props: Record<string, any>
attrs: Record<string, any>
propsDefaults: Record<string, any> | null
slots: StaticSlots
// to hold vnode props / slots in vdom interop mode
rawPropsRef?: ShallowRef<any>
rawSlotsRef?: ShallowRef<any>
emit: EmitFn
emitted: Record<string, boolean> | null
2024-12-10 17:00:35 +08:00
expose: (exposed: Record<string, any>) => void
exposed: Record<string, any> | null
exposeProxy: 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 23:37:40 +08:00
hmrReload?: (newComp: VaporComponent) => void
2024-12-04 13:50:54 +08:00
propsOptions?: NormalizedPropsOptions
emitsOptions?: ObjectEmitsOptions | null
2024-12-08 23:37:40 +08:00
isSingleRoot?: boolean
2024-12-07 15:12:32 +08:00
constructor(
comp: VaporComponent,
rawProps?: RawProps | null,
rawSlots?: RawSlots | null,
2024-12-10 12:49:47 +08:00
appContext?: GenericAppContext,
2024-12-07 15:12:32 +08:00
) {
this.vapor = true
2024-12-04 13:50:54 +08:00
this.uid = nextUid()
this.type = comp
2025-02-04 21:38:09 +08:00
this.parent = currentInstance
this.root = currentInstance ? currentInstance.root : this
2024-12-08 21:22:51 +08:00
if (currentInstance) {
this.appContext = currentInstance.appContext
this.provides = currentInstance.provides
this.ids = currentInstance.ids
} else {
2024-12-10 12:49:47 +08:00
this.appContext = appContext || emptyContext
2024-12-08 21:22:51 +08:00
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)
2024-12-10 17:00:35 +08:00
this.expose = expose.bind(null, this)
this.refs = EMPTY_OBJ
2024-12-10 17:00:35 +08:00
this.emitted =
this.exposed =
this.exposeProxy =
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.attrs = new Proxy(this, attrsHandlers)
2024-12-09 22:04:15 +08:00
this.props = comp.props
? new Proxy(this, propsHandlers!)
: isFunction(comp)
? this.attrs
: EMPTY_OBJ
} 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
}
/**
* Expose `getKeysFromRawProps` on the instance so it can be used in code
* paths where it's needed, e.g. `useModel`
*/
rawKeys(): string[] {
return getKeysFromRawProps(this.rawProps)
}
}
2024-12-04 13:50:54 +08:00
export function isVaporComponent(
value: unknown,
): value is VaporComponentInstance {
return value instanceof VaporComponentInstance
}
/**
* 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,
2025-02-04 21:38:09 +08:00
rawProps?: LooseRawProps | null,
rawSlots?: LooseRawSlots | null,
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
}
const el = document.createElement(comp)
// mark single root
;(el as any).$root = isSingleRoot
2024-12-06 11:10:35 +08:00
if (rawProps) {
renderEffect(() => {
2025-02-04 21:38:09 +08:00
setDynamicProps(el, [resolveDynamicProps(rawProps as RawProps)])
2024-12-06 11:10:35 +08:00
})
}
2024-12-07 21:43:08 +08:00
if (rawSlots) {
if (rawSlots.$) {
// TODO dynamic slot fragment
} else {
2025-02-04 21:38:09 +08:00
insert(getSlot(rawSlots as RawSlots, 'default')!(), el)
2024-12-07 21:43:08 +08:00
}
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,
2024-12-08 21:22:51 +08:00
): void {
2024-12-10 12:49:47 +08:00
if (__DEV__) {
startMeasure(instance, `mount`)
}
2025-02-12 22:01:28 +08:00
if (instance.bm) invokeArrayFns(instance.bm)
insert(instance.block, parent, anchor)
if (instance.m) queuePostFlushCb(() => invokeArrayFns(instance.m!))
instance.isMounted = true
2024-12-10 12:49:47 +08:00
if (__DEV__) {
endMeasure(instance, `mount`)
}
2024-12-08 21:22:51 +08:00
}
export function unmountComponent(
instance: VaporComponentInstance,
parentNode?: ParentNode,
2024-12-08 21:22:51 +08:00
): void {
if (instance.isMounted && !instance.isUnmounted) {
if (__DEV__ && instance.type.__hmrId) {
unregisterHMR(instance)
}
if (instance.bum) {
invokeArrayFns(instance.bum)
}
instance.scope.stop()
2024-12-10 18:43:26 +08:00
if (instance.um) {
queuePostFlushCb(() => invokeArrayFns(instance.um!))
}
2024-12-08 21:22:51 +08:00
instance.isUnmounted = true
}
if (parentNode) {
remove(instance.block, parentNode)
2024-12-08 21:22:51 +08:00
}
}
2024-12-10 17:00:35 +08:00
export function getExposed(
instance: GenericComponentInstance,
): Record<string, any> | undefined {
if (instance.exposed) {
return (
instance.exposeProxy ||
2024-12-12 22:34:35 +08:00
(instance.exposeProxy = new Proxy(markRaw(instance.exposed), {
get: (target, key) => unref(target[key as any]),
}))
2024-12-10 17:00:35 +08:00
)
}
}