mirror of https://github.com/vuejs/core.git
586 lines
15 KiB
TypeScript
586 lines
15 KiB
TypeScript
import {
|
|
type ComponentInternalOptions,
|
|
type ComponentPropsOptions,
|
|
EffectScope,
|
|
type EmitFn,
|
|
type EmitsOptions,
|
|
ErrorCodes,
|
|
type GenericAppContext,
|
|
type GenericComponentInstance,
|
|
type LifecycleHook,
|
|
type NormalizedPropsOptions,
|
|
type ObjectEmitsOptions,
|
|
type SuspenseBoundary,
|
|
callWithErrorHandling,
|
|
currentInstance,
|
|
endMeasure,
|
|
expose,
|
|
isKeepAlive,
|
|
nextUid,
|
|
popWarningContext,
|
|
pushWarningContext,
|
|
queuePostFlushCb,
|
|
registerHMR,
|
|
simpleSetCurrentInstance,
|
|
startMeasure,
|
|
unregisterHMR,
|
|
warn,
|
|
} from '@vue/runtime-dom'
|
|
import { type Block, insert, isBlock, remove } from './block'
|
|
import {
|
|
type ShallowRef,
|
|
markRaw,
|
|
onScopeDispose,
|
|
pauseTracking,
|
|
proxyRefs,
|
|
resetTracking,
|
|
unref,
|
|
} from '@vue/reactivity'
|
|
import {
|
|
EMPTY_OBJ,
|
|
ShapeFlags,
|
|
invokeArrayFns,
|
|
isFunction,
|
|
isString,
|
|
} from '@vue/shared'
|
|
import {
|
|
type DynamicPropsSource,
|
|
type RawProps,
|
|
getKeysFromRawProps,
|
|
getPropsProxyHandlers,
|
|
hasFallthroughAttrs,
|
|
normalizePropsOptions,
|
|
resolveDynamicProps,
|
|
setupPropsValidation,
|
|
} from './componentProps'
|
|
import { renderEffect } from './renderEffect'
|
|
import { emit, normalizeEmitsOptions } from './componentEmits'
|
|
import { setDynamicProps } from './dom/prop'
|
|
import {
|
|
type DynamicSlotSource,
|
|
type RawSlots,
|
|
type StaticSlots,
|
|
type VaporSlot,
|
|
dynamicSlotsProxyHandlers,
|
|
getSlot,
|
|
} from './componentSlots'
|
|
import { hmrReload, hmrRerender } from './hmr'
|
|
import { isHydrating, locateHydrationNode } from './dom/hydration'
|
|
import { insertionAnchor, insertionParent } from './insertionState'
|
|
import type { KeepAliveInstance } from './components/KeepAlive'
|
|
|
|
export { currentInstance } from '@vue/runtime-dom'
|
|
|
|
export type VaporComponent = FunctionalVaporComponent | ObjectVaporComponent
|
|
|
|
export type VaporSetupFn = (
|
|
props: any,
|
|
ctx: Pick<VaporComponentInstance, 'slots' | 'attrs' | 'emit' | 'expose'>,
|
|
) => Block | Record<string, any> | undefined
|
|
|
|
export type FunctionalVaporComponent = VaporSetupFn &
|
|
Omit<ObjectVaporComponent, 'setup'> & {
|
|
displayName?: string
|
|
} & SharedInternalOptions
|
|
|
|
export interface ObjectVaporComponent
|
|
extends ComponentInternalOptions,
|
|
SharedInternalOptions {
|
|
setup?: VaporSetupFn
|
|
inheritAttrs?: boolean
|
|
props?: ComponentPropsOptions
|
|
emits?: EmitsOptions
|
|
render?(
|
|
ctx: any,
|
|
props?: any,
|
|
emit?: EmitFn,
|
|
attrs?: any,
|
|
slots?: Record<string, VaporSlot>,
|
|
): Block
|
|
|
|
name?: string
|
|
vapor?: boolean
|
|
}
|
|
|
|
interface SharedInternalOptions {
|
|
/**
|
|
* Cached normalized props options.
|
|
* In vapor mode there are no mixins so normalized options can be cached
|
|
* directly on the component
|
|
*/
|
|
__propsOptions?: NormalizedPropsOptions
|
|
/**
|
|
* Cached normalized props proxy handlers.
|
|
*/
|
|
__propsHandlers?: [ProxyHandler<any> | null, ProxyHandler<any>]
|
|
/**
|
|
* Cached normalized emits options.
|
|
*/
|
|
__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.
|
|
export type LooseRawProps = Record<
|
|
string,
|
|
(() => unknown) | DynamicPropsSource[]
|
|
> & {
|
|
$?: DynamicPropsSource[]
|
|
}
|
|
|
|
export type LooseRawSlots = Record<string, VaporSlot | DynamicSlotSource[]> & {
|
|
$?: DynamicSlotSource[]
|
|
}
|
|
|
|
export function createComponent(
|
|
component: VaporComponent,
|
|
rawProps?: LooseRawProps | null,
|
|
rawSlots?: LooseRawSlots | null,
|
|
isSingleRoot?: boolean,
|
|
appContext: GenericAppContext = (currentInstance &&
|
|
currentInstance.appContext) ||
|
|
emptyContext,
|
|
): VaporComponentInstance {
|
|
const _insertionParent = insertionParent
|
|
const _insertionAnchor = insertionAnchor
|
|
if (isHydrating) {
|
|
locateHydrationNode()
|
|
}
|
|
|
|
// try to get the cached instance if inside keep-alive
|
|
if (currentInstance && isKeepAlive(currentInstance)) {
|
|
const cached = (currentInstance as KeepAliveInstance).getCachedInstance(
|
|
component,
|
|
)
|
|
if (cached) return cached
|
|
}
|
|
|
|
// vdom interop enabled and component is not an explicit vapor component
|
|
if (appContext.vapor && !component.__vapor) {
|
|
const frag = appContext.vapor.vdomMount(
|
|
component as any,
|
|
rawProps,
|
|
rawSlots,
|
|
)
|
|
if (!isHydrating && _insertionParent) {
|
|
insert(frag, _insertionParent, _insertionAnchor)
|
|
}
|
|
return frag
|
|
}
|
|
|
|
if (
|
|
isSingleRoot &&
|
|
component.inheritAttrs !== false &&
|
|
isVaporComponent(currentInstance) &&
|
|
currentInstance.hasFallthrough
|
|
) {
|
|
// check if we are the single root of the parent
|
|
// if yes, inject parent attrs as dynamic props source
|
|
const attrs = currentInstance.attrs
|
|
if (rawProps) {
|
|
;((rawProps as RawProps).$ || ((rawProps as RawProps).$ = [])).push(
|
|
() => attrs,
|
|
)
|
|
} else {
|
|
rawProps = { $: [() => attrs] } as RawProps
|
|
}
|
|
}
|
|
|
|
const instance = new VaporComponentInstance(
|
|
component,
|
|
rawProps as RawProps,
|
|
rawSlots as RawSlots,
|
|
appContext,
|
|
)
|
|
|
|
if (__DEV__) {
|
|
pushWarningContext(instance)
|
|
startMeasure(instance, `init`)
|
|
|
|
// cache normalized options for dev only emit check
|
|
instance.propsOptions = normalizePropsOptions(component)
|
|
instance.emitsOptions = normalizeEmitsOptions(component)
|
|
}
|
|
|
|
const prev = currentInstance
|
|
simpleSetCurrentInstance(instance)
|
|
pauseTracking()
|
|
|
|
if (__DEV__) {
|
|
setupPropsValidation(instance)
|
|
}
|
|
|
|
const setupFn = isFunction(component) ? component : component.setup
|
|
const setupResult = setupFn
|
|
? callWithErrorHandling(setupFn, instance, ErrorCodes.SETUP_FUNCTION, [
|
|
instance.props,
|
|
instance,
|
|
]) || 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 {
|
|
instance.devtoolsRawSetupState = setupResult
|
|
// TODO make the proxy warn non-existent property access during dev
|
|
instance.setupState = proxyRefs(setupResult)
|
|
devRender(instance)
|
|
|
|
// HMR
|
|
if (component.__hmrId) {
|
|
registerHMR(instance)
|
|
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
|
|
}
|
|
}
|
|
|
|
// 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
|
|
})
|
|
}
|
|
|
|
resetTracking()
|
|
simpleSetCurrentInstance(prev, instance)
|
|
|
|
if (__DEV__) {
|
|
popWarningContext()
|
|
endMeasure(instance, 'init')
|
|
}
|
|
|
|
onScopeDispose(() => unmountComponent(instance), true)
|
|
|
|
if (!isHydrating && _insertionParent) {
|
|
mountComponent(instance, _insertionParent, _insertionAnchor)
|
|
}
|
|
|
|
return instance
|
|
}
|
|
|
|
export let isApplyingFallthroughProps = false
|
|
|
|
/**
|
|
* dev only
|
|
*/
|
|
export function devRender(instance: VaporComponentInstance): void {
|
|
instance.block =
|
|
callWithErrorHandling(
|
|
instance.type.render!,
|
|
instance,
|
|
ErrorCodes.RENDER_FUNCTION,
|
|
[
|
|
instance.setupState,
|
|
instance.props,
|
|
instance.emit,
|
|
instance.attrs,
|
|
instance.slots,
|
|
],
|
|
) || []
|
|
}
|
|
|
|
const emptyContext: GenericAppContext = {
|
|
app: null as any,
|
|
config: {},
|
|
provides: /*@__PURE__*/ Object.create(null),
|
|
}
|
|
|
|
export class VaporComponentInstance implements GenericComponentInstance {
|
|
vapor: true
|
|
uid: number
|
|
type: VaporComponent
|
|
root: GenericComponentInstance | null
|
|
parent: GenericComponentInstance | null
|
|
appContext: GenericAppContext
|
|
|
|
block: Block
|
|
scope: EffectScope
|
|
|
|
rawProps: RawProps
|
|
rawSlots: RawSlots
|
|
|
|
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
|
|
|
|
expose: (exposed: Record<string, any>) => void
|
|
exposed: Record<string, any> | null
|
|
exposeProxy: Record<string, any> | null
|
|
|
|
// for useTemplateRef()
|
|
refs: Record<string, any>
|
|
// for provide / inject
|
|
provides: Record<string, any>
|
|
// for useId
|
|
ids: [string, number, number]
|
|
// for suspense
|
|
suspense: SuspenseBoundary | null
|
|
|
|
hasFallthrough: boolean
|
|
|
|
// lifecycle hooks
|
|
isMounted: boolean
|
|
isUnmounted: boolean
|
|
isDeactivated: boolean
|
|
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
|
|
|
|
// dev only
|
|
setupState?: Record<string, any>
|
|
devtoolsRawSetupState?: any
|
|
hmrRerender?: () => void
|
|
hmrReload?: (newComp: VaporComponent) => void
|
|
propsOptions?: NormalizedPropsOptions
|
|
emitsOptions?: ObjectEmitsOptions | null
|
|
isSingleRoot?: boolean
|
|
shapeFlag?: number
|
|
|
|
constructor(
|
|
comp: VaporComponent,
|
|
rawProps?: RawProps | null,
|
|
rawSlots?: RawSlots | null,
|
|
appContext?: GenericAppContext,
|
|
) {
|
|
this.vapor = true
|
|
this.uid = nextUid()
|
|
this.type = comp
|
|
this.parent = currentInstance
|
|
this.root = currentInstance ? currentInstance.root : this
|
|
|
|
if (currentInstance) {
|
|
this.appContext = currentInstance.appContext
|
|
this.provides = currentInstance.provides
|
|
this.ids = currentInstance.ids
|
|
} else {
|
|
this.appContext = appContext || emptyContext
|
|
this.provides = Object.create(this.appContext.provides)
|
|
this.ids = ['', 0, 0]
|
|
}
|
|
|
|
this.block = null! // to be set
|
|
this.scope = new EffectScope(true)
|
|
|
|
this.emit = emit.bind(null, this)
|
|
this.expose = expose.bind(null, this)
|
|
this.refs = EMPTY_OBJ
|
|
this.emitted =
|
|
this.exposed =
|
|
this.exposeProxy =
|
|
this.propsDefaults =
|
|
this.suspense =
|
|
null
|
|
|
|
this.isMounted =
|
|
this.isUnmounted =
|
|
this.isUpdating =
|
|
this.isDeactivated =
|
|
false
|
|
|
|
// init props
|
|
this.rawProps = rawProps || EMPTY_OBJ
|
|
this.hasFallthrough = hasFallthroughAttrs(comp, rawProps)
|
|
if (rawProps || comp.props) {
|
|
const [propsHandlers, attrsHandlers] = getPropsProxyHandlers(comp)
|
|
this.attrs = new Proxy(this, attrsHandlers)
|
|
this.props = comp.props
|
|
? new Proxy(this, propsHandlers!)
|
|
: isFunction(comp)
|
|
? this.attrs
|
|
: EMPTY_OBJ
|
|
} else {
|
|
this.props = this.attrs = EMPTY_OBJ
|
|
}
|
|
|
|
// init slots
|
|
this.rawSlots = rawSlots || EMPTY_OBJ
|
|
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)
|
|
}
|
|
}
|
|
|
|
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.
|
|
*/
|
|
export function createComponentWithFallback(
|
|
comp: VaporComponent | string,
|
|
rawProps?: LooseRawProps | null,
|
|
rawSlots?: LooseRawSlots | null,
|
|
isSingleRoot?: boolean,
|
|
): HTMLElement | VaporComponentInstance {
|
|
if (!isString(comp)) {
|
|
return createComponent(comp, rawProps, rawSlots, isSingleRoot)
|
|
}
|
|
|
|
const el = document.createElement(comp)
|
|
// mark single root
|
|
;(el as any).$root = isSingleRoot
|
|
|
|
if (rawProps) {
|
|
renderEffect(() => {
|
|
setDynamicProps(el, [resolveDynamicProps(rawProps as RawProps)])
|
|
})
|
|
}
|
|
|
|
if (rawSlots) {
|
|
if (rawSlots.$) {
|
|
// TODO dynamic slot fragment
|
|
} else {
|
|
insert(getSlot(rawSlots as RawSlots, 'default')!(), el)
|
|
}
|
|
}
|
|
|
|
return el
|
|
}
|
|
|
|
export function mountComponent(
|
|
instance: VaporComponentInstance,
|
|
parentNode: ParentNode,
|
|
anchor?: Node | null | 0,
|
|
): void {
|
|
if (instance.shapeFlag! & ShapeFlags.COMPONENT_KEPT_ALIVE) {
|
|
;(instance.parent as KeepAliveInstance).activate(
|
|
instance,
|
|
parentNode,
|
|
anchor as any,
|
|
)
|
|
instance.isMounted = true
|
|
return
|
|
}
|
|
|
|
if (__DEV__) {
|
|
startMeasure(instance, `mount`)
|
|
}
|
|
if (instance.bm) invokeArrayFns(instance.bm)
|
|
insert(instance.block, parentNode, anchor)
|
|
if (instance.m) queuePostFlushCb(instance.m!)
|
|
if (
|
|
instance.shapeFlag! & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE &&
|
|
instance.a
|
|
) {
|
|
queuePostFlushCb(instance.a!)
|
|
}
|
|
instance.isMounted = true
|
|
if (__DEV__) {
|
|
endMeasure(instance, `mount`)
|
|
}
|
|
}
|
|
|
|
export function unmountComponent(
|
|
instance: VaporComponentInstance,
|
|
parentNode?: ParentNode,
|
|
): void {
|
|
if (instance.shapeFlag! & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
|
|
;(instance.parent as KeepAliveInstance).deactivate(instance)
|
|
return
|
|
}
|
|
|
|
if (instance.isMounted && !instance.isUnmounted) {
|
|
if (__DEV__ && instance.type.__hmrId) {
|
|
unregisterHMR(instance)
|
|
}
|
|
if (instance.bum) {
|
|
invokeArrayFns(instance.bum)
|
|
}
|
|
|
|
instance.scope.stop()
|
|
|
|
if (instance.um) {
|
|
queuePostFlushCb(instance.um!)
|
|
}
|
|
instance.isUnmounted = true
|
|
}
|
|
|
|
if (parentNode) {
|
|
remove(instance.block, parentNode)
|
|
}
|
|
}
|
|
|
|
export function getExposed(
|
|
instance: GenericComponentInstance,
|
|
): Record<string, any> | undefined {
|
|
if (instance.exposed) {
|
|
return (
|
|
instance.exposeProxy ||
|
|
(instance.exposeProxy = new Proxy(markRaw(instance.exposed), {
|
|
get: (target, key) => unref(target[key as any]),
|
|
}))
|
|
)
|
|
}
|
|
}
|