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
|
* avoid unnecessary watcher trigger
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
propsDefaults: Data
|
propsDefaults: Data | null
|
||||||
/**
|
/**
|
||||||
* setup related
|
* setup related
|
||||||
* @internal
|
* @internal
|
||||||
|
@ -647,7 +647,7 @@ export function createComponentInstance(
|
||||||
emitted: null,
|
emitted: null,
|
||||||
|
|
||||||
// props default value
|
// props default value
|
||||||
propsDefaults: EMPTY_OBJ,
|
propsDefaults: null,
|
||||||
|
|
||||||
// inheritAttrs
|
// inheritAttrs
|
||||||
inheritAttrs: type.inheritAttrs,
|
inheritAttrs: type.inheritAttrs,
|
||||||
|
|
|
@ -282,11 +282,10 @@ export function updateProps(
|
||||||
const camelizedKey = camelize(key)
|
const camelizedKey = camelize(key)
|
||||||
props[camelizedKey] = resolvePropValue(
|
props[camelizedKey] = resolvePropValue(
|
||||||
options,
|
options,
|
||||||
rawCurrentProps,
|
|
||||||
camelizedKey,
|
camelizedKey,
|
||||||
value,
|
value,
|
||||||
instance,
|
instance,
|
||||||
false /* isAbsent */,
|
baseResolveDefault,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -331,10 +330,10 @@ export function updateProps(
|
||||||
) {
|
) {
|
||||||
props[key] = resolvePropValue(
|
props[key] = resolvePropValue(
|
||||||
options,
|
options,
|
||||||
rawCurrentProps,
|
|
||||||
key,
|
key,
|
||||||
undefined,
|
undefined,
|
||||||
instance,
|
instance,
|
||||||
|
baseResolveDefault,
|
||||||
true /* isAbsent */,
|
true /* isAbsent */,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -428,16 +427,15 @@ function setFullProps(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needCastKeys) {
|
if (needCastKeys) {
|
||||||
const rawCurrentProps = toRaw(props)
|
|
||||||
const castValues = rawCastValues || EMPTY_OBJ
|
const castValues = rawCastValues || EMPTY_OBJ
|
||||||
for (let i = 0; i < needCastKeys.length; i++) {
|
for (let i = 0; i < needCastKeys.length; i++) {
|
||||||
const key = needCastKeys[i]
|
const key = needCastKeys[i]
|
||||||
props[key] = resolvePropValue(
|
props[key] = resolvePropValue(
|
||||||
options!,
|
options!,
|
||||||
rawCurrentProps,
|
|
||||||
key,
|
key,
|
||||||
castValues[key],
|
castValues[key],
|
||||||
instance,
|
instance,
|
||||||
|
baseResolveDefault,
|
||||||
!hasOwn(castValues, key),
|
!hasOwn(castValues, key),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -446,14 +444,32 @@ function setFullProps(
|
||||||
return hasAttrsChanged
|
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,
|
options: NormalizedProps,
|
||||||
props: Data,
|
|
||||||
key: string,
|
key: string,
|
||||||
value: unknown,
|
value: unknown,
|
||||||
instance: ComponentInternalInstance,
|
instance: T,
|
||||||
isAbsent: boolean,
|
/**
|
||||||
) {
|
* Allow runtime-specific default resolution logic
|
||||||
|
*/
|
||||||
|
resolveDefault: (
|
||||||
|
factory: (props: Data) => unknown,
|
||||||
|
instance: T,
|
||||||
|
key: string,
|
||||||
|
) => unknown,
|
||||||
|
isAbsent = false,
|
||||||
|
): unknown {
|
||||||
const opt = options[key]
|
const opt = options[key]
|
||||||
if (opt != null) {
|
if (opt != null) {
|
||||||
const hasDefault = hasOwn(opt, 'default')
|
const hasDefault = hasOwn(opt, 'default')
|
||||||
|
@ -465,19 +481,16 @@ function resolvePropValue(
|
||||||
!opt.skipFactory &&
|
!opt.skipFactory &&
|
||||||
isFunction(defaultValue)
|
isFunction(defaultValue)
|
||||||
) {
|
) {
|
||||||
const { propsDefaults } = instance
|
const cachedDefaults =
|
||||||
if (key in propsDefaults) {
|
instance.propsDefaults || (instance.propsDefaults = {})
|
||||||
value = propsDefaults[key]
|
if (hasOwn(cachedDefaults, key)) {
|
||||||
|
value = cachedDefaults[key]
|
||||||
} else {
|
} else {
|
||||||
const reset = setCurrentInstance(instance)
|
value = cachedDefaults[key] = resolveDefault(
|
||||||
value = propsDefaults[key] = defaultValue.call(
|
defaultValue,
|
||||||
__COMPAT__ &&
|
instance,
|
||||||
isCompatEnabled(DeprecationTypes.PROPS_DEFAULT_THIS, instance)
|
key,
|
||||||
? createPropsDefaultThis(instance, props, key)
|
|
||||||
: null,
|
|
||||||
props,
|
|
||||||
)
|
)
|
||||||
reset()
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
value = defaultValue
|
value = defaultValue
|
||||||
|
@ -502,6 +515,27 @@ function resolvePropValue(
|
||||||
return value
|
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>()
|
const mixinPropsCache = new WeakMap<ConcreteComponent, NormalizedPropsOptions>()
|
||||||
|
|
||||||
export function normalizePropsOptions(
|
export function normalizePropsOptions(
|
||||||
|
@ -550,6 +584,22 @@ export function normalizePropsOptions(
|
||||||
return EMPTY_ARR as any
|
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)) {
|
if (isArray(raw)) {
|
||||||
for (let i = 0; i < raw.length; i++) {
|
for (let i = 0; i < raw.length; i++) {
|
||||||
if (__DEV__ && !isString(raw[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) {
|
function validatePropName(key: string) {
|
||||||
|
|
|
@ -320,6 +320,7 @@ export type {
|
||||||
ExtractPropTypes,
|
ExtractPropTypes,
|
||||||
ExtractPublicPropTypes,
|
ExtractPublicPropTypes,
|
||||||
ExtractDefaultPropTypes,
|
ExtractDefaultPropTypes,
|
||||||
|
NormalizedPropsOptions,
|
||||||
} from './componentProps'
|
} from './componentProps'
|
||||||
export type {
|
export type {
|
||||||
Directive,
|
Directive,
|
||||||
|
@ -480,3 +481,10 @@ export const compatUtils = (
|
||||||
export const DeprecationTypes = (
|
export const DeprecationTypes = (
|
||||||
__COMPAT__ ? _DeprecationTypes : null
|
__COMPAT__ ? _DeprecationTypes : null
|
||||||
) as typeof _DeprecationTypes
|
) 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 { isArray } from '@vue/shared'
|
||||||
import {
|
import { type ComponentInstance, isVaporComponent } from './_new/component'
|
||||||
type ComponentInstance,
|
|
||||||
isVaporComponent,
|
|
||||||
} from './apiCreateComponentSimple'
|
|
||||||
|
|
||||||
export const fragmentKey: unique symbol = Symbol(__DEV__ ? `fragmentKey` : ``)
|
export const fragmentKey: unique symbol = Symbol(__DEV__ ? `fragmentKey` : ``)
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { isArray } from '@vue/shared'
|
||||||
import { renderEffect } from '../renderEffect'
|
import { renderEffect } from '../renderEffect'
|
||||||
import { setText } from './prop'
|
import { setText } from './prop'
|
||||||
import { type Block, normalizeBlock } from '../block'
|
import { type Block, normalizeBlock } from '../block'
|
||||||
import { isVaporComponent } from '../apiCreateComponentSimple'
|
import { isVaporComponent } from '../_new/component'
|
||||||
|
|
||||||
// export function insert(
|
// export function insert(
|
||||||
// block: Block,
|
// block: Block,
|
||||||
|
|
|
@ -155,11 +155,6 @@ export {
|
||||||
export { createBranch, createIf } from './apiCreateIf'
|
export { createBranch, createIf } from './apiCreateIf'
|
||||||
export { createFor, createForSlots } from './apiCreateFor'
|
export { createFor, createForSlots } from './apiCreateFor'
|
||||||
export { createComponent } from './apiCreateComponent'
|
export { createComponent } from './apiCreateComponent'
|
||||||
export {
|
|
||||||
createComponentSimple,
|
|
||||||
renderEffectSimple,
|
|
||||||
createVaporAppSimple,
|
|
||||||
} from './apiCreateComponentSimple'
|
|
||||||
export { createSelector } from './apiCreateSelector'
|
export { createSelector } from './apiCreateSelector'
|
||||||
export { setInheritAttrs } from './componentAttrs'
|
export { setInheritAttrs } from './componentAttrs'
|
||||||
|
|
||||||
|
@ -195,3 +190,5 @@ export const devtools = (
|
||||||
export const setDevtoolsHook = (
|
export const setDevtoolsHook = (
|
||||||
__DEV__ || __ESM_BUNDLER__ ? _setDevtoolsHook : NOOP
|
__DEV__ || __ESM_BUNDLER__ ? _setDevtoolsHook : NOOP
|
||||||
) as typeof _setDevtoolsHook
|
) as typeof _setDevtoolsHook
|
||||||
|
|
||||||
|
export * from './_new'
|
||||||
|
|
Loading…
Reference in New Issue