wip: props casting

This commit is contained in:
Evan You 2024-12-02 23:51:48 +08:00
parent 0986051f12
commit 435ca32ff8
No known key found for this signature in database
GPG Key ID: 00E9AB7A6704CE0A
2 changed files with 96 additions and 71 deletions

View File

@ -9,23 +9,26 @@ import {
type ComponentInternalInstance, type ComponentInternalInstance,
SetupContext, SetupContext,
} from './component' } from './component'
import { EMPTY_OBJ, NO, hasOwn, isFunction } from '@vue/shared' import { NO, camelize, hasOwn, isFunction } from '@vue/shared'
import { type SchedulerJob, queueJob } from '../../runtime-core/src/scheduler' import { type SchedulerJob, queueJob } from '../../runtime-core/src/scheduler'
import { insert } from './dom/element' import { insert } from './dom/element'
import { normalizeContainer } from './apiRender' import { normalizeContainer } from './apiRender'
import { normalizePropsOptions } from './componentProps' import { normalizePropsOptions, resolvePropValue } from './componentProps'
import type { Block } from './block'
interface RawProps { interface RawProps {
[key: string]: any [key: string]: PropSource
$?: DynamicPropsSource[] $?: DynamicPropsSource[]
} }
type DynamicPropsSource = Record<string, any> | (() => Record<string, any>) type PropSource<T = any> = T | (() => T)
type DynamicPropsSource = PropSource<Record<string, any>>
export function createComponentSimple( export function createComponentSimple(
component: Component, component: Component,
rawProps?: RawProps, rawProps?: RawProps,
): any { ): Block {
const instance = new ComponentInstance( const instance = new ComponentInstance(
component, component,
rawProps, rawProps,
@ -39,11 +42,10 @@ export function createComponentSimple(
const setupFn = isFunction(component) ? component : component.setup const setupFn = isFunction(component) ? component : component.setup
const setupContext = setupFn!.length > 1 ? new SetupContext(instance) : null const setupContext = setupFn!.length > 1 ? new SetupContext(instance) : null
const node = setupFn!( const node = setupFn!(
// TODO __DEV__ ? shallowReadonly(props) :
instance.props, instance.props,
// @ts-expect-error // @ts-expect-error
setupContext, setupContext,
) ) as Block
// single root, inherit attrs // single root, inherit attrs
// let i // let i
@ -74,48 +76,26 @@ export class ComponentInstance {
attrs: Record<string, any> attrs: Record<string, any>
constructor(comp: Component, rawProps?: RawProps) { constructor(comp: Component, rawProps?: RawProps) {
this.type = comp this.type = comp
// init props // init props
// TODO fast path for all static props
let mayHaveFallthroughAttrs = false let mayHaveFallthroughAttrs = false
if (rawProps && comp.props) { if (comp.props && rawProps && rawProps.$) {
if (rawProps.$) { // has dynamic props, use proxy
// has dynamic props, use full proxy const handlers = getDynamicPropsHandlers(comp, this)
const handlers = getPropsProxyHandlers(comp) this.props = new Proxy(rawProps, handlers[0])
this.props = new Proxy(rawProps, handlers[0]) this.attrs = new Proxy(rawProps, handlers[1])
this.attrs = new Proxy(rawProps, handlers[1]) mayHaveFallthroughAttrs = true
mayHaveFallthroughAttrs = true
} else {
// fast path for all static prop keys
this.props = rawProps
this.attrs = {}
const propsOptions = normalizePropsOptions(comp)[0]!
for (const key in propsOptions) {
if (!(key in rawProps)) {
rawProps[key] = undefined // TODO default value / casting
} else {
// TODO override getter with default value / casting
}
}
for (const key in rawProps) {
if (!(key in propsOptions)) {
Object.defineProperty(
this.attrs,
key,
Object.getOwnPropertyDescriptor(rawProps, key)!,
)
delete rawProps[key]
mayHaveFallthroughAttrs = true
}
}
}
} else { } else {
this.props = EMPTY_OBJ mayHaveFallthroughAttrs = initStaticProps(
this.attrs = rawProps || EMPTY_OBJ comp,
mayHaveFallthroughAttrs = !!rawProps rawProps,
(this.props = {}),
(this.attrs = {}),
)
} }
// TODO validate props
if (mayHaveFallthroughAttrs) { if (mayHaveFallthroughAttrs) {
// TODO apply fallthrough attrs // TODO apply fallthrough attrs
} }
@ -123,36 +103,95 @@ export class 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 // TODO optimization: maybe convert functions into computeds
function resolveSource(source: DynamicPropsSource): Record<string, any> { function resolveSource(source: PropSource): Record<string, any> {
return isFunction(source) ? source() : source return isFunction(source) ? source() : source
} }
function getPropsProxyHandlers( function getDynamicPropsHandlers(
comp: Component, comp: Component,
instance: ComponentInstance,
): [ProxyHandler<RawProps>, ProxyHandler<RawProps>] { ): [ProxyHandler<RawProps>, ProxyHandler<RawProps>] {
if (comp.__propsHandlers) { if (comp.__propsHandlers) {
return comp.__propsHandlers return comp.__propsHandlers
} }
let normalizedKeys: string[] | undefined let normalizedKeys: string[] | undefined
const normalizedOptions = normalizePropsOptions(comp)[0]! const propsOptions = normalizePropsOptions(comp)[0]!
const isProp = (key: string | symbol) => hasOwn(normalizedOptions, key) const isProp = (key: string | symbol) => hasOwn(propsOptions, key)
const getProp = (target: RawProps, key: string | symbol, asProp: boolean) => { const getProp = (target: RawProps, key: string | symbol, asProp: boolean) => {
if (key !== '$' && (asProp ? isProp(key) : !isProp(key))) { if (key !== '$' && (asProp ? isProp(key) : !isProp(key))) {
if (hasOwn(target, 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. // TODO default value, casting, etc.
return target[key] return castProp(resolveSource(target[key as string]))
} }
if (target.$) { if (target.$) {
let source, resolved let source, resolved
for (source of target.$) { for (source of target.$) {
resolved = resolveSource(source) resolved = resolveSource(source)
if (hasOwn(resolved, key)) { if (hasOwn(resolved, key)) {
return resolved[key] return castProp(resolved[key])
} }
} }
} }
return castProp(undefined, true)
} }
} }
@ -169,10 +208,9 @@ function getPropsProxyHandlers(
} }
}, },
ownKeys: () => ownKeys: () =>
normalizedKeys || (normalizedKeys = Object.keys(normalizedOptions)), normalizedKeys || (normalizedKeys = Object.keys(propsOptions)),
set: NO, set: NO,
deleteProperty: NO, deleteProperty: NO,
// TODO dev traps to prevent mutation
} satisfies ProxyHandler<RawProps> } satisfies ProxyHandler<RawProps>
const hasAttr = (target: RawProps, key: string | symbol) => { const hasAttr = (target: RawProps, key: string | symbol) => {

View File

@ -11,11 +11,7 @@ import {
import type { Data } from '@vue/runtime-shared' import type { Data } from '@vue/runtime-shared'
import { shallowReactive } from '@vue/reactivity' import { shallowReactive } from '@vue/reactivity'
import { warn } from './warning' import { warn } from './warning'
import { import type { Component, ComponentInternalInstance } from './component'
type Component,
type ComponentInternalInstance,
setCurrentInstance,
} from './component'
import { patchAttrs } from './componentAttrs' import { patchAttrs } from './componentAttrs'
import { firstEffect } from './renderEffect' import { firstEffect } from './renderEffect'
@ -144,7 +140,7 @@ function registerProp(
const [options, needCastKeys] = instance.propsOptions const [options, needCastKeys] = instance.propsOptions
const needCast = needCastKeys && needCastKeys.includes(key) const needCast = needCastKeys && needCastKeys.includes(key)
const withCast = (value: unknown, absent?: boolean) => const withCast = (value: unknown, absent?: boolean) =>
resolvePropValue(options!, props, key, value, instance, absent) resolvePropValue(options!, props, key, value, absent)
if (isAbsent) { if (isAbsent) {
props[key] = needCast ? withCast(undefined, true) : undefined props[key] = needCast ? withCast(undefined, true) : undefined
@ -209,14 +205,13 @@ export function getDynamicPropValue(
return [undefined, true] return [undefined, true]
} }
function resolvePropValue( export function resolvePropValue(
options: NormalizedProps, options: NormalizedProps,
props: Data, props: Data,
key: string, key: string,
value: unknown, value: unknown,
instance: ComponentInternalInstance,
isAbsent?: boolean, isAbsent?: boolean,
) { ): 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')
@ -228,15 +223,7 @@ function resolvePropValue(
!opt.skipFactory && !opt.skipFactory &&
isFunction(defaultValue) isFunction(defaultValue)
) { ) {
// TODO: caching? value = defaultValue.call(null, props)
// const { propsDefaults } = instance
// if (key in propsDefaults) {
// value = propsDefaults[key]
// } else {
const reset = setCurrentInstance(instance)
instance.scope.run(() => (value = defaultValue.call(null, props)))
reset()
// }
} else { } else {
value = defaultValue value = defaultValue
} }