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

294 lines
7.7 KiB
TypeScript

import {
EffectScope,
ReactiveEffect,
pauseTracking,
resetTracking,
} from '@vue/reactivity'
import {
type Component,
type ComponentInternalInstance,
SetupContext,
} from './component'
import { NO, camelize, hasOwn, isFunction } from '@vue/shared'
import { type SchedulerJob, queueJob } from '../../runtime-core/src/scheduler'
import { insert } from './dom/element'
import { normalizeContainer } from './apiRender'
import { normalizePropsOptions, resolvePropValue } from './componentProps'
import type { Block } from './block'
interface RawProps {
[key: string]: PropSource
$?: DynamicPropsSource[]
}
type PropSource<T = any> = T | (() => T)
type DynamicPropsSource = PropSource<Record<string, any>>
export function createComponentSimple(
component: Component,
rawProps?: RawProps,
): Block {
const instance = new ComponentInstance(
component,
rawProps,
) as any as ComponentInternalInstance
pauseTracking()
let prevInstance = currentInstance
currentInstance = instance
instance.scope.on()
const setupFn = isFunction(component) ? component : component.setup
const setupContext = setupFn!.length > 1 ? new SetupContext(instance) : null
const node = setupFn!(
instance.props,
// @ts-expect-error
setupContext,
) as Block
// single root, inherit attrs
if (
rawProps &&
component.inheritAttrs !== false &&
node instanceof Element &&
Object.keys(instance.attrs).length
) {
renderEffectSimple(() => {
// TODO
})
}
instance.scope.off()
currentInstance = prevInstance
resetTracking()
// @ts-expect-error
node.__vue__ = instance
return node
}
let uid = 0
let currentInstance: ComponentInstance | null = null
export class ComponentInstance {
type: Component
uid: number = uid++
scope: EffectScope = new EffectScope(true)
props: Record<string, any>
attrs: Record<string, any>
constructor(comp: Component, rawProps?: RawProps) {
this.type = comp
// init props
let mayHaveFallthroughAttrs = false
if (comp.props && rawProps && rawProps.$) {
// has dynamic props, use proxy
const handlers = getDynamicPropsHandlers(comp, this)
this.props = new Proxy(rawProps, handlers[0])
this.attrs = new Proxy(rawProps, handlers[1])
mayHaveFallthroughAttrs = true
} else {
mayHaveFallthroughAttrs = initStaticProps(
comp,
rawProps,
(this.props = {}),
(this.attrs = {}),
)
}
// TODO validate props
if (mayHaveFallthroughAttrs) {
// TODO apply fallthrough attrs
}
// TODO init slots
}
}
function initStaticProps(
comp: Component,
rawProps: RawProps | undefined,
props: any,
attrs: any,
): boolean {
let hasAttrs = false
const [propsOptions, needCastKeys] = normalizePropsOptions(comp)
for (const key in rawProps) {
const normalizedKey = camelize(key)
const needCast = needCastKeys && needCastKeys.includes(normalizedKey)
const source = rawProps[key]
if (propsOptions && normalizedKey in propsOptions) {
if (isFunction(source)) {
Object.defineProperty(props, normalizedKey, {
enumerable: true,
get: needCast
? () =>
resolvePropValue(propsOptions, props, normalizedKey, source())
: source,
})
} else {
props[normalizedKey] = needCast
? resolvePropValue(propsOptions, props, normalizedKey, source)
: source
}
} else {
if (isFunction(source)) {
Object.defineProperty(attrs, key, {
enumerable: true,
get: source,
})
} else {
attrs[normalizedKey] = source
}
hasAttrs = true
}
}
for (const key in propsOptions) {
if (!(key in props)) {
props[key] = resolvePropValue(propsOptions, props, key, undefined, true)
}
}
return hasAttrs
}
// TODO optimization: maybe convert functions into computeds
function resolveSource(source: PropSource): Record<string, any> {
return isFunction(source) ? source() : source
}
function getDynamicPropsHandlers(
comp: Component,
instance: ComponentInstance,
): [ProxyHandler<RawProps>, ProxyHandler<RawProps>] {
if (comp.__propsHandlers) {
return comp.__propsHandlers
}
let normalizedKeys: string[] | undefined
const propsOptions = normalizePropsOptions(comp)[0]!
const isProp = (key: string | symbol) => hasOwn(propsOptions, key)
const getProp = (target: RawProps, key: string | symbol, asProp: boolean) => {
if (key !== '$' && (asProp ? isProp(key) : !isProp(key))) {
const castProp = (value: any, isAbsent?: boolean) =>
asProp
? resolvePropValue(
propsOptions,
instance.props,
key as string,
value,
isAbsent,
)
: value
if (key in target) {
// TODO default value, casting, etc.
return castProp(resolveSource(target[key as string]))
}
if (target.$) {
let source, resolved
for (source of target.$) {
resolved = resolveSource(source)
if (hasOwn(resolved, key)) {
return castProp(resolved[key])
}
}
}
return castProp(undefined, true)
}
}
const propsHandlers = {
get: (target, key) => getProp(target, key, true),
has: (_, key) => isProp(key),
getOwnPropertyDescriptor(target, key) {
if (isProp(key)) {
return {
configurable: true,
enumerable: true,
get: () => getProp(target, key, true),
}
}
},
ownKeys: () =>
normalizedKeys || (normalizedKeys = Object.keys(propsOptions)),
set: NO,
deleteProperty: NO,
} satisfies ProxyHandler<RawProps>
const hasAttr = (target: RawProps, key: string | symbol) => {
if (key === '$' || isProp(key)) return false
if (hasOwn(target, key)) return true
if (target.$) {
let source, resolved
for (source of target.$) {
resolved = resolveSource(source)
if (hasOwn(resolved, key)) {
return true
}
}
}
return false
}
const attrsHandlers = {
get: (target, key) => getProp(target, key, false),
has: hasAttr,
getOwnPropertyDescriptor(target, key) {
if (hasAttr(target, key)) {
return {
configurable: true,
enumerable: true,
get: () => getProp(target, key, false),
}
}
},
ownKeys(target) {
const staticKeys = Object.keys(target).filter(
key => key !== '$' && !isProp(key),
)
if (target.$) {
for (const source of target.$) {
staticKeys.push(...Object.keys(resolveSource(source)))
}
}
return staticKeys
},
set: NO,
deleteProperty: NO,
} satisfies ProxyHandler<RawProps>
return (comp.__propsHandlers = [propsHandlers, attrsHandlers])
}
export function renderEffectSimple(fn: () => void): void {
const updateFn = () => {
fn()
}
const effect = new ReactiveEffect(updateFn)
const job: SchedulerJob = effect.runIfDirty.bind(effect)
job.i = currentInstance as any
job.id = currentInstance!.uid
effect.scheduler = () => queueJob(job)
effect.run()
// TODO lifecycle
// TODO recurse handling
// TODO measure
}
// vapor app can be a subset of main app APIs
// TODO refactor core createApp for reuse
export function createVaporAppSimple(comp: Component): any {
return {
mount(container: string | ParentNode) {
container = normalizeContainer(container)
// clear content before mounting
if (container.nodeType === 1 /* Node.ELEMENT_NODE */) {
container.textContent = ''
}
const rootBlock = createComponentSimple(comp)
insert(rootBlock, container)
},
}
}