wip: props handling

This commit is contained in:
Evan You 2024-12-02 20:35:45 +08:00
parent 0acafc7b4d
commit 41c18ef272
No known key found for this signature in database
GPG Key ID: 00E9AB7A6704CE0A
6 changed files with 222 additions and 91 deletions

View File

@ -2,36 +2,63 @@ import {
EffectScope, EffectScope,
ReactiveEffect, ReactiveEffect,
pauseTracking, pauseTracking,
proxyRefs,
resetTracking, resetTracking,
} from '@vue/reactivity' } from '@vue/reactivity'
import { import {
type Component, type Component,
type ComponentInternalInstance, type ComponentInternalInstance,
createSetupContext, SetupContext,
} from './component' } from './component'
import { EMPTY_OBJ, isFunction } from '@vue/shared' import { EMPTY_OBJ, NO, 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 { normalizeContainer } from './apiRender'
import { normalizePropsOptions } from './componentProps'
export function createComponentSimple(component: any, rawProps?: any): any { interface RawProps {
[key: string]: any
$?: DynamicPropsSource[]
}
type DynamicPropsSource = Record<string, any> | (() => Record<string, any>)
export function createComponentSimple(
component: Component,
rawProps?: RawProps,
): any {
const instance = new ComponentInstance( const instance = new ComponentInstance(
component, component,
rawProps, rawProps,
) as any as ComponentInternalInstance ) as any as ComponentInternalInstance
pauseTracking() pauseTracking()
let prevInstance = currentInstance let prevInstance = currentInstance
currentInstance = instance currentInstance = instance
instance.scope.on() instance.scope.on()
const setupFn = isFunction(component) ? component : component.setup const setupFn = isFunction(component) ? component : component.setup
const setupContext = setupFn.length > 1 ? createSetupContext(instance) : null const setupContext = setupFn!.length > 1 ? new SetupContext(instance) : null
const node = setupFn( const node = setupFn!(
// TODO __DEV__ ? shallowReadonly(props) : // TODO __DEV__ ? shallowReadonly(props) :
instance.props, instance.props,
// @ts-expect-error
setupContext, setupContext,
) )
// single root, inherit attrs
// let i
// if (component.inheritAttrs !== false && node instanceof Element) {
// renderEffectSimple(() => {
// // for (const key in instance.attrs) {
// // i = key
// // }
// })
// }
instance.scope.off() instance.scope.off()
currentInstance = prevInstance currentInstance = prevInstance
resetTracking() resetTracking()
// @ts-expect-error
node.__vue__ = instance node.__vue__ = instance
return node return node
} }
@ -40,18 +67,159 @@ let uid = 0
let currentInstance: ComponentInstance | null = null let currentInstance: ComponentInstance | null = null
export class ComponentInstance { export class ComponentInstance {
type: any type: Component
uid: number = uid++ uid: number = uid++
scope: EffectScope = new EffectScope(true) scope: EffectScope = new EffectScope(true)
props: any props: Record<string, any>
constructor(comp: Component, rawProps: any) { attrs: Record<string, any>
constructor(comp: Component, rawProps?: RawProps) {
this.type = comp this.type = comp
// init props // init props
this.props = rawProps ? proxyRefs(rawProps) : EMPTY_OBJ
// TODO fast path for all static props
let mayHaveFallthroughAttrs = false
if (rawProps && comp.props) {
if (rawProps.$) {
// has dynamic props, use full proxy
const handlers = getPropsProxyHandlers(comp)
this.props = new Proxy(rawProps, handlers[0])
this.attrs = new Proxy(rawProps, handlers[1])
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 {
this.props = EMPTY_OBJ
this.attrs = rawProps || EMPTY_OBJ
mayHaveFallthroughAttrs = !!rawProps
}
if (mayHaveFallthroughAttrs) {
// TODO apply fallthrough attrs
}
// TODO init slots // TODO init slots
} }
} }
// TODO optimization: maybe convert functions into computeds
function resolveSource(source: DynamicPropsSource): Record<string, any> {
return isFunction(source) ? source() : source
}
function getPropsProxyHandlers(
comp: Component,
): [ProxyHandler<RawProps>, ProxyHandler<RawProps>] {
if (comp.__propsHandlers) {
return comp.__propsHandlers
}
let normalizedKeys: string[] | undefined
const normalizedOptions = normalizePropsOptions(comp)[0]!
const isProp = (key: string | symbol) => hasOwn(normalizedOptions, key)
const getProp = (target: RawProps, key: string | symbol, asProp: boolean) => {
if (key !== '$' && (asProp ? isProp(key) : !isProp(key))) {
if (hasOwn(target, key)) {
// TODO default value, casting, etc.
return target[key]
}
if (target.$) {
let source, resolved
for (source of target.$) {
resolved = resolveSource(source)
if (hasOwn(resolved, key)) {
return resolved[key]
}
}
}
}
}
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(normalizedOptions)),
set: NO,
deleteProperty: NO,
// TODO dev traps to prevent mutation
} 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 { export function renderEffectSimple(fn: () => void): void {
const updateFn = () => { const updateFn = () => {
fn() fn()
@ -67,3 +235,19 @@ export function renderEffectSimple(fn: () => void): void {
// TODO recurse handling // TODO recurse handling
// TODO measure // 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)
},
}
}

View File

@ -28,14 +28,20 @@ import type { Data } from '@vue/runtime-shared'
export type Component = FunctionalComponent | ObjectComponent export type Component = FunctionalComponent | ObjectComponent
type SharedInternalOptions = {
__propsOptions?: NormalizedPropsOptions
__propsHandlers?: [ProxyHandler<any>, ProxyHandler<any>]
}
export type SetupFn = ( export type SetupFn = (
props: any, props: any,
ctx: SetupContext, ctx: SetupContext,
) => Block | Data | undefined ) => Block | Data | undefined
export type FunctionalComponent = SetupFn & export type FunctionalComponent = SetupFn &
Omit<ObjectComponent, 'setup'> & { Omit<ObjectComponent, 'setup'> & {
displayName?: string displayName?: string
} } & SharedInternalOptions
export class SetupContext<E = EmitsOptions> { export class SetupContext<E = EmitsOptions> {
attrs: Data attrs: Data
@ -96,7 +102,9 @@ export function createSetupContext(
} }
} }
export interface ObjectComponent extends ComponentInternalOptions { export interface ObjectComponent
extends ComponentInternalOptions,
SharedInternalOptions {
setup?: SetupFn setup?: SetupFn
inheritAttrs?: boolean inheritAttrs?: boolean
props?: ComponentPropsOptions props?: ComponentPropsOptions

View File

@ -257,7 +257,8 @@ function resolvePropValue(
} }
export function normalizePropsOptions(comp: Component): NormalizedPropsOptions { export function normalizePropsOptions(comp: Component): NormalizedPropsOptions {
// TODO: cahching? const cached = comp.__propsOptions
if (cached) return cached
const raw = comp.props const raw = comp.props
const normalized: NormalizedProps | undefined = {} const normalized: NormalizedProps | undefined = {}
@ -296,7 +297,10 @@ export function normalizePropsOptions(comp: Component): NormalizedPropsOptions {
} }
} }
const res: NormalizedPropsOptions = [normalized, needCastKeys] const res: NormalizedPropsOptions = (comp.__propsOptions = [
normalized,
needCastKeys,
])
return res return res
} }

View File

@ -158,6 +158,7 @@ export { createComponent } from './apiCreateComponent'
export { export {
createComponentSimple, createComponentSimple,
renderEffectSimple, renderEffectSimple,
createVaporAppSimple,
} from './apiCreateComponentSimple' } from './apiCreateComponentSimple'
export { createSelector } from './apiCreateSelector' export { createSelector } from './apiCreateSelector'
export { setInheritAttrs } from './componentAttrs' export { setInheritAttrs } from './componentAttrs'

View File

@ -1,79 +1,13 @@
import { import { createComponentSimple, createVaporAppSimple } from 'vue/vapor'
createComponentSimple, import List from './list'
// createFor, import Props from './props'
createVaporApp, import './style.css'
delegate,
delegateEvents,
ref,
renderEffectSimple,
template,
} from 'vue/vapor'
function createForSimple(val: () => any, render: (i: number) => any) {
const l = val(),
arr = new Array(l)
for (let i = 0; i < l; i++) {
arr[i] = render(i)
}
return arr
}
const t0 = template('<h1>Vapor</h1>')
const App = {
vapor: true,
__name: 'App',
setup() {
return (_ctx => {
const n0 = t0()
const n1 = createForSimple(
() => 10000,
(i: number) => createComponentSimple(Comp, { count: i }),
)
return [n0, createComponentSimple(Counter), n1]
})()
},
}
const Counter = {
vapor: true,
__name: 'Counter',
setup() {
delegateEvents('click')
const count = ref(0)
const button = document.createElement('button')
button.textContent = '++'
delegate(button, 'click', () => () => count.value++)
return [
button,
createComponentSimple(Comp, {
// if ref
count,
// if exp
get plusOne() {
return count.value + 1
},
}),
// TODO dynamic props: merge with Proxy that iterates sources on access
]
},
}
const t0$1 = template('<div></div>')
const Comp = {
vapor: true,
__name: 'Comp',
setup(props: any) {
return (_ctx => {
const n = t0$1()
renderEffectSimple(() => {
n.textContent = props.count + ' / ' + props.plusOne
})
return n
})()
},
}
const s = performance.now() const s = performance.now()
const app = createVaporApp(App) const app = createVaporAppSimple({
setup() {
return [createComponentSimple(Props), createComponentSimple(List)]
},
})
app.mount('#app') app.mount('#app')
console.log((performance.now() - s).toFixed(2)) console.log((performance.now() - s).toFixed(2))

View File

@ -1,3 +1,3 @@
html { .red {
color-scheme: light dark; color: red;
} }