refactor: reuse props logic from core
This commit is contained in:
parent
f8046a3e1a
commit
783d8b4d0d
|
@ -470,7 +470,7 @@ export interface ComponentInternalInstance {
|
|||
* avoid unnecessary watcher trigger
|
||||
* @internal
|
||||
*/
|
||||
propsDefaults: Data
|
||||
propsDefaults: Data | null
|
||||
/**
|
||||
* setup related
|
||||
* @internal
|
||||
|
@ -647,7 +647,7 @@ export function createComponentInstance(
|
|||
emitted: null,
|
||||
|
||||
// props default value
|
||||
propsDefaults: EMPTY_OBJ,
|
||||
propsDefaults: null,
|
||||
|
||||
// inheritAttrs
|
||||
inheritAttrs: type.inheritAttrs,
|
||||
|
|
|
@ -282,11 +282,10 @@ export function updateProps(
|
|||
const camelizedKey = camelize(key)
|
||||
props[camelizedKey] = resolvePropValue(
|
||||
options,
|
||||
rawCurrentProps,
|
||||
camelizedKey,
|
||||
value,
|
||||
instance,
|
||||
false /* isAbsent */,
|
||||
baseResolveDefault,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
|
@ -331,10 +330,10 @@ export function updateProps(
|
|||
) {
|
||||
props[key] = resolvePropValue(
|
||||
options,
|
||||
rawCurrentProps,
|
||||
key,
|
||||
undefined,
|
||||
instance,
|
||||
baseResolveDefault,
|
||||
true /* isAbsent */,
|
||||
)
|
||||
}
|
||||
|
@ -428,16 +427,15 @@ function setFullProps(
|
|||
}
|
||||
|
||||
if (needCastKeys) {
|
||||
const rawCurrentProps = toRaw(props)
|
||||
const castValues = rawCastValues || EMPTY_OBJ
|
||||
for (let i = 0; i < needCastKeys.length; i++) {
|
||||
const key = needCastKeys[i]
|
||||
props[key] = resolvePropValue(
|
||||
options!,
|
||||
rawCurrentProps,
|
||||
key,
|
||||
castValues[key],
|
||||
instance,
|
||||
baseResolveDefault,
|
||||
!hasOwn(castValues, key),
|
||||
)
|
||||
}
|
||||
|
@ -446,14 +444,32 @@ function setFullProps(
|
|||
return hasAttrsChanged
|
||||
}
|
||||
|
||||
function resolvePropValue(
|
||||
/**
|
||||
* A type that allows both vdom and vapor instances
|
||||
*/
|
||||
type CommonInstance = Pick<
|
||||
ComponentInternalInstance,
|
||||
'props' | 'propsDefaults' | 'ce'
|
||||
>
|
||||
|
||||
/**
|
||||
* @internal for runtime-vapor
|
||||
*/
|
||||
export function resolvePropValue<T extends CommonInstance>(
|
||||
options: NormalizedProps,
|
||||
props: Data,
|
||||
key: string,
|
||||
value: unknown,
|
||||
instance: ComponentInternalInstance,
|
||||
isAbsent: boolean,
|
||||
) {
|
||||
instance: T,
|
||||
/**
|
||||
* Allow runtime-specific default resolution logic
|
||||
*/
|
||||
resolveDefault: (
|
||||
factory: (props: Data) => unknown,
|
||||
instance: T,
|
||||
key: string,
|
||||
) => unknown,
|
||||
isAbsent = false,
|
||||
): unknown {
|
||||
const opt = options[key]
|
||||
if (opt != null) {
|
||||
const hasDefault = hasOwn(opt, 'default')
|
||||
|
@ -465,19 +481,16 @@ function resolvePropValue(
|
|||
!opt.skipFactory &&
|
||||
isFunction(defaultValue)
|
||||
) {
|
||||
const { propsDefaults } = instance
|
||||
if (key in propsDefaults) {
|
||||
value = propsDefaults[key]
|
||||
const cachedDefaults =
|
||||
instance.propsDefaults || (instance.propsDefaults = {})
|
||||
if (hasOwn(cachedDefaults, key)) {
|
||||
value = cachedDefaults[key]
|
||||
} else {
|
||||
const reset = setCurrentInstance(instance)
|
||||
value = propsDefaults[key] = defaultValue.call(
|
||||
__COMPAT__ &&
|
||||
isCompatEnabled(DeprecationTypes.PROPS_DEFAULT_THIS, instance)
|
||||
? createPropsDefaultThis(instance, props, key)
|
||||
: null,
|
||||
props,
|
||||
value = cachedDefaults[key] = resolveDefault(
|
||||
defaultValue,
|
||||
instance,
|
||||
key,
|
||||
)
|
||||
reset()
|
||||
}
|
||||
} else {
|
||||
value = defaultValue
|
||||
|
@ -502,6 +515,27 @@ function resolvePropValue(
|
|||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* runtime-dom-specific default resolving logic
|
||||
*/
|
||||
function baseResolveDefault(
|
||||
factory: (props: Data) => unknown,
|
||||
instance: ComponentInternalInstance,
|
||||
key: string,
|
||||
) {
|
||||
let value
|
||||
const reset = setCurrentInstance(instance)
|
||||
const props = toRaw(instance.props)
|
||||
value = factory.call(
|
||||
__COMPAT__ && isCompatEnabled(DeprecationTypes.PROPS_DEFAULT_THIS, instance)
|
||||
? createPropsDefaultThis(instance, props, key)
|
||||
: null,
|
||||
props,
|
||||
)
|
||||
reset()
|
||||
return value
|
||||
}
|
||||
|
||||
const mixinPropsCache = new WeakMap<ConcreteComponent, NormalizedPropsOptions>()
|
||||
|
||||
export function normalizePropsOptions(
|
||||
|
@ -550,6 +584,22 @@ export function normalizePropsOptions(
|
|||
return EMPTY_ARR as any
|
||||
}
|
||||
|
||||
baseNormalizePropsOptions(raw, normalized, needCastKeys)
|
||||
const res: NormalizedPropsOptions = [normalized, needCastKeys]
|
||||
if (isObject(comp)) {
|
||||
cache.set(comp, res)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal for runtime-vapor only
|
||||
*/
|
||||
export function baseNormalizePropsOptions(
|
||||
raw: ComponentPropsOptions | undefined,
|
||||
normalized: NonNullable<NormalizedPropsOptions[0]>,
|
||||
needCastKeys: NonNullable<NormalizedPropsOptions[1]>,
|
||||
): void {
|
||||
if (isArray(raw)) {
|
||||
for (let i = 0; i < raw.length; i++) {
|
||||
if (__DEV__ && !isString(raw[i])) {
|
||||
|
@ -604,12 +654,6 @@ export function normalizePropsOptions(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
const res: NormalizedPropsOptions = [normalized, needCastKeys]
|
||||
if (isObject(comp)) {
|
||||
cache.set(comp, res)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
function validatePropName(key: string) {
|
||||
|
|
|
@ -320,6 +320,7 @@ export type {
|
|||
ExtractPropTypes,
|
||||
ExtractPublicPropTypes,
|
||||
ExtractDefaultPropTypes,
|
||||
NormalizedPropsOptions,
|
||||
} from './componentProps'
|
||||
export type {
|
||||
Directive,
|
||||
|
@ -480,3 +481,10 @@ export const compatUtils = (
|
|||
export const DeprecationTypes = (
|
||||
__COMPAT__ ? _DeprecationTypes : null
|
||||
) as typeof _DeprecationTypes
|
||||
|
||||
// VAPOR -----------------------------------------------------------------------
|
||||
|
||||
// **IMPORTANT** These APIs are exposed solely for @vue/runtime-vapor and may
|
||||
// change without notice between versions. User code should never rely on them.
|
||||
|
||||
export { baseNormalizePropsOptions, resolvePropValue } from './componentProps'
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import { normalizeContainer } from '../apiRender'
|
||||
import { insert } from '../dom/element'
|
||||
import { type Component, createComponent } from './component'
|
||||
|
||||
export function createVaporApp(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 instance = createComponent(comp)
|
||||
insert(instance.block, container)
|
||||
return instance
|
||||
},
|
||||
}
|
||||
}
|
|
@ -0,0 +1,184 @@
|
|||
import {
|
||||
type ComponentPropsOptions,
|
||||
EffectScope,
|
||||
type EmitsOptions,
|
||||
type NormalizedPropsOptions,
|
||||
} from '@vue/runtime-core'
|
||||
import type { Block } from '../block'
|
||||
import type { Data } from '@vue/runtime-shared'
|
||||
import { pauseTracking, resetTracking } from '@vue/reactivity'
|
||||
import { isFunction } from '@vue/shared'
|
||||
import {
|
||||
type RawProps,
|
||||
getDynamicPropsHandlers,
|
||||
initStaticProps,
|
||||
} from './componentProps'
|
||||
import { setDynamicProp } from '../dom/prop'
|
||||
import { renderEffect } from './renderEffect'
|
||||
|
||||
export type Component = FunctionalComponent | ObjectComponent
|
||||
|
||||
export type SetupFn = (
|
||||
props: any,
|
||||
ctx: SetupContext,
|
||||
) => Block | Data | undefined
|
||||
|
||||
export type FunctionalComponent = SetupFn &
|
||||
Omit<ObjectComponent, 'setup'> & {
|
||||
displayName?: string
|
||||
} & SharedInternalOptions
|
||||
|
||||
export interface ObjectComponent
|
||||
extends ComponentInternalOptions,
|
||||
SharedInternalOptions {
|
||||
setup?: SetupFn
|
||||
inheritAttrs?: boolean
|
||||
props?: ComponentPropsOptions
|
||||
emits?: EmitsOptions
|
||||
render?(ctx: any): Block
|
||||
|
||||
name?: string
|
||||
vapor?: boolean
|
||||
}
|
||||
|
||||
interface SharedInternalOptions {
|
||||
__propsOptions?: NormalizedPropsOptions
|
||||
__propsHandlers?: [ProxyHandler<any>, ProxyHandler<any>]
|
||||
}
|
||||
|
||||
// Note: can't mark this whole interface internal because some public interfaces
|
||||
// extend it.
|
||||
interface ComponentInternalOptions {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
__scopeId?: string
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
__cssModules?: Data
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
__hmrId?: string
|
||||
/**
|
||||
* Compat build only, for bailing out of certain compatibility behavior
|
||||
*/
|
||||
__isBuiltIn?: boolean
|
||||
/**
|
||||
* This one should be exposed so that devtools can make use of it
|
||||
*/
|
||||
__file?: string
|
||||
/**
|
||||
* name inferred from filename
|
||||
*/
|
||||
__name?: string
|
||||
}
|
||||
|
||||
export function createComponent(
|
||||
component: Component,
|
||||
rawProps?: RawProps,
|
||||
isSingleRoot?: boolean,
|
||||
): ComponentInstance {
|
||||
// check if we are the single root of the parent
|
||||
// if yes, inject parent attrs as dynamic props source
|
||||
if (isSingleRoot && currentInstance && currentInstance.hasFallthrough) {
|
||||
if (rawProps) {
|
||||
;(rawProps.$ || (rawProps.$ = [])).push(currentInstance.attrs)
|
||||
} else {
|
||||
rawProps = { $: [currentInstance.attrs] }
|
||||
}
|
||||
}
|
||||
|
||||
const instance = new ComponentInstance(component, rawProps)
|
||||
|
||||
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
|
||||
instance.block = setupFn!(
|
||||
instance.props,
|
||||
// @ts-expect-error
|
||||
setupContext,
|
||||
) as Block // TODO handle return object
|
||||
|
||||
// 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])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
instance.scope.off()
|
||||
currentInstance = prevInstance
|
||||
resetTracking()
|
||||
return instance
|
||||
}
|
||||
|
||||
let uid = 0
|
||||
export let currentInstance: ComponentInstance | null = null
|
||||
|
||||
export class ComponentInstance {
|
||||
type: Component
|
||||
uid: number = uid++
|
||||
scope: EffectScope = new EffectScope(true)
|
||||
props: Record<string, any>
|
||||
propsDefaults: Record<string, any> | null
|
||||
attrs: Record<string, any>
|
||||
block: Block
|
||||
exposed?: Record<string, any>
|
||||
hasFallthrough: boolean
|
||||
|
||||
constructor(comp: Component, rawProps?: RawProps) {
|
||||
this.type = comp
|
||||
this.block = null! // to be set
|
||||
|
||||
// init props
|
||||
this.propsDefaults = null
|
||||
this.hasFallthrough = 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])
|
||||
this.hasFallthrough = true
|
||||
} else {
|
||||
this.props = {}
|
||||
this.attrs = {}
|
||||
this.hasFallthrough = initStaticProps(comp, rawProps, this)
|
||||
}
|
||||
|
||||
// TODO validate props
|
||||
// TODO init slots
|
||||
}
|
||||
}
|
||||
|
||||
export function isVaporComponent(value: unknown): value is ComponentInstance {
|
||||
return value instanceof ComponentInstance
|
||||
}
|
||||
|
||||
export class SetupContext<E = EmitsOptions> {
|
||||
attrs: Record<string, any>
|
||||
// emit: EmitFn<E>
|
||||
// slots: Readonly<StaticSlots>
|
||||
expose: (exposed?: Record<string, any>) => void
|
||||
|
||||
constructor(instance: ComponentInstance) {
|
||||
this.attrs = instance.attrs
|
||||
// this.emit = instance.emit as EmitFn<E>
|
||||
// this.slots = instance.slots
|
||||
this.expose = (exposed = {}) => {
|
||||
instance.exposed = exposed
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,214 @@
|
|||
import { EMPTY_ARR, NO, camelize, hasOwn, isFunction } from '@vue/shared'
|
||||
import type { Component, ComponentInstance } from './component'
|
||||
import {
|
||||
type NormalizedPropsOptions,
|
||||
baseNormalizePropsOptions,
|
||||
resolvePropValue,
|
||||
} from '@vue/runtime-core'
|
||||
|
||||
export interface RawProps {
|
||||
[key: string]: PropSource
|
||||
$?: DynamicPropsSource[]
|
||||
}
|
||||
|
||||
type PropSource<T = any> = T | (() => T)
|
||||
|
||||
type DynamicPropsSource = PropSource<Record<string, any>>
|
||||
|
||||
export function initStaticProps(
|
||||
comp: Component,
|
||||
rawProps: RawProps | undefined,
|
||||
instance: ComponentInstance,
|
||||
): boolean {
|
||||
let hasAttrs = false
|
||||
const { props, attrs } = instance
|
||||
const [propsOptions, needCastKeys] = normalizePropsOptions(comp)
|
||||
// TODO emits filtering
|
||||
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,
|
||||
normalizedKey,
|
||||
source(),
|
||||
instance,
|
||||
resolveDefault,
|
||||
)
|
||||
: source,
|
||||
})
|
||||
} else {
|
||||
props[normalizedKey] = needCast
|
||||
? resolvePropValue(
|
||||
propsOptions,
|
||||
normalizedKey,
|
||||
source,
|
||||
instance,
|
||||
resolveDefault,
|
||||
)
|
||||
: 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,
|
||||
key,
|
||||
undefined,
|
||||
instance,
|
||||
resolveDefault,
|
||||
true,
|
||||
)
|
||||
}
|
||||
}
|
||||
return hasAttrs
|
||||
}
|
||||
|
||||
function resolveDefault(
|
||||
factory: (props: Record<string, any>) => unknown,
|
||||
instance: ComponentInstance,
|
||||
) {
|
||||
return factory.call(null, instance.props)
|
||||
}
|
||||
|
||||
// TODO optimization: maybe convert functions into computeds
|
||||
function resolveSource(source: PropSource): Record<string, any> {
|
||||
return isFunction(source) ? source() : source
|
||||
}
|
||||
|
||||
export 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 = false) =>
|
||||
asProp
|
||||
? resolvePropValue(
|
||||
propsOptions,
|
||||
key as string,
|
||||
value,
|
||||
instance,
|
||||
resolveDefault,
|
||||
isAbsent,
|
||||
)
|
||||
: value
|
||||
|
||||
if (key in target) {
|
||||
// TODO default value, casting, etc.
|
||||
return castProp(resolveSource(target[key as string]))
|
||||
}
|
||||
if (target.$) {
|
||||
let i = target.$.length
|
||||
let source
|
||||
while (i--) {
|
||||
source = resolveSource(target.$[i])
|
||||
if (hasOwn(source, key)) {
|
||||
return castProp(source[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 i = target.$.length
|
||||
while (i--) {
|
||||
if (hasOwn(resolveSource(target.$[i]), 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.$) {
|
||||
let i = target.$.length
|
||||
while (i--) {
|
||||
staticKeys.push(...Object.keys(resolveSource(target.$[i])))
|
||||
}
|
||||
}
|
||||
return staticKeys
|
||||
},
|
||||
set: NO,
|
||||
deleteProperty: NO,
|
||||
} satisfies ProxyHandler<RawProps>
|
||||
|
||||
return (comp.__propsHandlers = [propsHandlers, attrsHandlers])
|
||||
}
|
||||
|
||||
function normalizePropsOptions(comp: Component): NormalizedPropsOptions {
|
||||
const cached = comp.__propsOptions
|
||||
if (cached) return cached
|
||||
|
||||
const raw = comp.props
|
||||
if (!raw) return EMPTY_ARR as []
|
||||
|
||||
const normalized: NormalizedPropsOptions[0] = {}
|
||||
const needCastKeys: NormalizedPropsOptions[1] = []
|
||||
baseNormalizePropsOptions(raw, normalized, needCastKeys)
|
||||
|
||||
return (comp.__propsOptions = [normalized, needCastKeys])
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export { createComponent as createComponentSimple } from './component'
|
||||
export { renderEffect as renderEffectSimple } from './renderEffect'
|
||||
export { createVaporApp as createVaporAppSimple } from './apiCreateApp'
|
|
@ -0,0 +1,22 @@
|
|||
import { ReactiveEffect } from '@vue/reactivity'
|
||||
import {
|
||||
type SchedulerJob,
|
||||
queueJob,
|
||||
} from '../../../runtime-core/src/scheduler'
|
||||
import { currentInstance } from './component'
|
||||
|
||||
export function renderEffect(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
|
||||
}
|
|
@ -1,322 +0,0 @@
|
|||
import {
|
||||
EffectScope,
|
||||
ReactiveEffect,
|
||||
pauseTracking,
|
||||
resetTracking,
|
||||
} from '@vue/reactivity'
|
||||
import type { Component } 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'
|
||||
import { EmitFn, type EmitsOptions } from './componentEmits'
|
||||
import { StaticSlots } from './componentSlots'
|
||||
import { setDynamicProp } from './dom/prop'
|
||||
|
||||
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,
|
||||
isSingleRoot?: boolean,
|
||||
): ComponentInstance {
|
||||
// check if we are the single root of the parent
|
||||
// if yes, inject parent attrs as dynamic props source
|
||||
if (isSingleRoot && currentInstance && currentInstance.hasFallthrough) {
|
||||
if (rawProps) {
|
||||
;(rawProps.$ || (rawProps.$ = [])).push(currentInstance.attrs)
|
||||
} else {
|
||||
rawProps = { $: [currentInstance.attrs] }
|
||||
}
|
||||
}
|
||||
|
||||
const instance = new ComponentInstance(component, rawProps)
|
||||
|
||||
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
|
||||
instance.block = setupFn!(
|
||||
instance.props,
|
||||
// @ts-expect-error
|
||||
setupContext,
|
||||
) as Block // TODO handle return object
|
||||
|
||||
// single root, inherit attrs
|
||||
if (
|
||||
instance.hasFallthrough &&
|
||||
component.inheritAttrs !== false &&
|
||||
instance.block instanceof Element &&
|
||||
Object.keys(instance.attrs).length
|
||||
) {
|
||||
renderEffectSimple(() => {
|
||||
for (const key in instance.attrs) {
|
||||
setDynamicProp(instance.block as Element, key, instance.attrs[key])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
instance.scope.off()
|
||||
currentInstance = prevInstance
|
||||
resetTracking()
|
||||
return instance
|
||||
}
|
||||
|
||||
class SetupContext<E = EmitsOptions> {
|
||||
attrs: Record<string, any>
|
||||
// emit: EmitFn<E>
|
||||
// slots: Readonly<StaticSlots>
|
||||
expose: (exposed?: Record<string, any>) => void
|
||||
|
||||
constructor(instance: ComponentInstance) {
|
||||
this.attrs = instance.attrs
|
||||
// this.emit = instance.emit as EmitFn<E>
|
||||
// this.slots = instance.slots
|
||||
this.expose = (exposed = {}) => {
|
||||
instance.exposed = exposed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
block: Block
|
||||
exposed?: Record<string, any>
|
||||
hasFallthrough: boolean
|
||||
|
||||
constructor(comp: Component, rawProps?: RawProps) {
|
||||
this.type = comp
|
||||
this.block = null! // to be set
|
||||
|
||||
// init props
|
||||
this.hasFallthrough = 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])
|
||||
this.hasFallthrough = true
|
||||
} else {
|
||||
this.hasFallthrough = initStaticProps(
|
||||
comp,
|
||||
rawProps,
|
||||
(this.props = {}),
|
||||
(this.attrs = {}),
|
||||
)
|
||||
}
|
||||
|
||||
// TODO validate props
|
||||
// TODO init slots
|
||||
}
|
||||
}
|
||||
|
||||
export function isVaporComponent(value: unknown): value is ComponentInstance {
|
||||
return value instanceof ComponentInstance
|
||||
}
|
||||
|
||||
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 i = target.$.length
|
||||
let source
|
||||
while (i--) {
|
||||
source = resolveSource(target.$[i])
|
||||
if (hasOwn(source, key)) {
|
||||
return castProp(source[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 i = target.$.length
|
||||
while (i--) {
|
||||
if (hasOwn(resolveSource(target.$[i]), 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.$) {
|
||||
let i = target.$.length
|
||||
while (i--) {
|
||||
staticKeys.push(...Object.keys(resolveSource(target.$[i])))
|
||||
}
|
||||
}
|
||||
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)
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1,8 +1,5 @@
|
|||
import { isArray } from '@vue/shared'
|
||||
import {
|
||||
type ComponentInstance,
|
||||
isVaporComponent,
|
||||
} from './apiCreateComponentSimple'
|
||||
import { type ComponentInstance, isVaporComponent } from './_new/component'
|
||||
|
||||
export const fragmentKey: unique symbol = Symbol(__DEV__ ? `fragmentKey` : ``)
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import { isArray } from '@vue/shared'
|
|||
import { renderEffect } from '../renderEffect'
|
||||
import { setText } from './prop'
|
||||
import { type Block, normalizeBlock } from '../block'
|
||||
import { isVaporComponent } from '../apiCreateComponentSimple'
|
||||
import { isVaporComponent } from '../_new/component'
|
||||
|
||||
// export function insert(
|
||||
// block: Block,
|
||||
|
|
|
@ -155,11 +155,6 @@ export {
|
|||
export { createBranch, createIf } from './apiCreateIf'
|
||||
export { createFor, createForSlots } from './apiCreateFor'
|
||||
export { createComponent } from './apiCreateComponent'
|
||||
export {
|
||||
createComponentSimple,
|
||||
renderEffectSimple,
|
||||
createVaporAppSimple,
|
||||
} from './apiCreateComponentSimple'
|
||||
export { createSelector } from './apiCreateSelector'
|
||||
export { setInheritAttrs } from './componentAttrs'
|
||||
|
||||
|
@ -195,3 +190,5 @@ export const devtools = (
|
|||
export const setDevtoolsHook = (
|
||||
__DEV__ || __ESM_BUNDLER__ ? _setDevtoolsHook : NOOP
|
||||
) as typeof _setDevtoolsHook
|
||||
|
||||
export * from './_new'
|
||||
|
|
Loading…
Reference in New Issue