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.

255 lines
6.6 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-04 13:50:54 +08:00
nextUid,
popWarningContext,
pushWarningContext,
warn,
2024-12-04 14:22:26 +08:00
} from '@vue/runtime-dom'
import { type Block, isBlock } from './block'
2024-12-04 13:50:54 +08:00
import { pauseTracking, resetTracking } from '@vue/reactivity'
2024-12-04 23:02:15 +08:00
import { EMPTY_OBJ, hasOwn, isFunction } from '@vue/shared'
import {
2024-12-04 13:50:54 +08:00
type RawProps,
2024-12-04 23:02:15 +08:00
getPropsProxyHandlers,
normalizePropsOptions,
2024-12-04 13:50:54 +08:00
} from './componentProps'
import { setDynamicProp } from './dom/prop'
import { renderEffect } from './renderEffect'
import { emit } from './componentEmits'
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): 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,
rawProps?: RawProps,
isSingleRoot?: boolean,
): VaporComponentInstance {
// check if we are the single root of the parent
// if yes, inject parent attrs as dynamic props source
if (isSingleRoot && 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-04 13:50:54 +08:00
const instance = new VaporComponentInstance(component, rawProps)
2024-12-04 13:50:54 +08:00
pauseTracking()
let prevInstance = currentInstance
currentInstance = instance
2024-12-04 13:50:54 +08:00
instance.scope.on()
if (__DEV__) {
pushWarningContext(instance)
}
const setupFn = isFunction(component) ? component : component.setup
const setupContext = setupFn!.length > 1 ? new SetupContext(instance) : null
const setupResult =
setupFn!(
instance.props,
// @ts-expect-error
setupContext,
) || 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 {
instance.block = component.render.call(null, setupResult)
}
} 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()
}
instance.scope.off()
currentInstance = prevInstance
resetTracking()
return instance
}
2024-12-04 13:50:54 +08:00
export let currentInstance: VaporComponentInstance | null = null
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 {
uid: number
2024-12-04 13:50:54 +08:00
type: VaporComponent
parent: GenericComponentInstance | null
appContext: GenericAppContext
2024-12-04 13:50:54 +08:00
block: Block
scope: EffectScope
2024-12-04 13:50:54 +08:00
rawProps: RawProps | undefined
props: Record<string, any>
attrs: Record<string, any>
exposed: Record<string, any> | null
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>
hasFallthrough: boolean
isMounted: boolean
isUnmounted: boolean
2024-12-04 13:50:54 +08:00
isDeactivated: boolean
// LifecycleHooks.ERROR_CAPTURED
ec: LifecycleHook
2024-12-04 13:50:54 +08:00
// dev only
setupState?: Record<string, any>
2024-12-04 13:50:54 +08:00
propsOptions?: NormalizedPropsOptions
emitsOptions?: ObjectEmitsOptions | null
2024-12-04 13:50:54 +08:00
constructor(comp: VaporComponent, rawProps?: RawProps) {
this.uid = nextUid()
this.type = comp
this.parent = currentInstance
this.appContext = currentInstance
? currentInstance.appContext
: emptyContext
2024-12-04 13:50:54 +08:00
this.block = null! // to be set
this.scope = new EffectScope(true)
2024-12-04 13:50:54 +08:00
this.rawProps = rawProps
this.provides = this.refs = EMPTY_OBJ
2024-12-04 23:02:15 +08:00
this.emitted = this.ec = this.exposed = this.propsDefaults = null
2024-12-04 13:50:54 +08:00
this.isMounted = this.isUnmounted = this.isDeactivated = false
// init props
2024-12-04 23:02:15 +08:00
const target = rawProps || EMPTY_OBJ
const handlers = getPropsProxyHandlers(comp, this)
this.props = comp.props ? new Proxy(target, handlers[0]!) : {}
this.attrs = new Proxy(target, handlers[1])
// determine fallthrough
2024-12-04 13:50:54 +08:00
this.hasFallthrough = false
2024-12-04 23:02:15 +08:00
if (rawProps) {
2024-12-04 23:47:28 +08:00
if (rawProps.$ || !comp.props) {
2024-12-04 23:02:15 +08:00
this.hasFallthrough = true
} else {
// check if rawProps contains any keys not declared
2024-12-04 23:47:28 +08:00
const propsOptions = normalizePropsOptions(comp)[0]
2024-12-04 23:02:15 +08:00
for (const key in rawProps) {
2024-12-04 23:47:28 +08:00
if (!hasOwn(propsOptions!, key)) {
2024-12-04 23:02:15 +08:00
this.hasFallthrough = true
break
}
}
}
2024-12-04 13:50:54 +08:00
}
2024-12-04 13:50:54 +08:00
// TODO validate props
// TODO init slots
}
}
2024-12-04 13:50:54 +08:00
export function isVaporComponent(
value: unknown,
): value is VaporComponentInstance {
return value instanceof VaporComponentInstance
}
2024-12-04 13:50:54 +08:00
export class SetupContext<E = EmitsOptions> {
attrs: Record<string, any>
emit: EmitFn<E>
2024-12-04 13:50:54 +08:00
// slots: Readonly<StaticSlots>
expose: (exposed?: Record<string, any>) => void
2024-12-04 13:50:54 +08:00
constructor(instance: VaporComponentInstance) {
this.attrs = instance.attrs
this.emit = emit.bind(null, instance) as EmitFn<E>
2024-12-04 13:50:54 +08:00
// this.slots = instance.slots
this.expose = (exposed = {}) => {
instance.exposed = exposed
}
}
}