From 41c18ef272b85b20bfa93f26f401bc057bc12a5e Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 2 Dec 2024 20:35:45 +0800 Subject: [PATCH] wip: props handling --- .../src/apiCreateComponentSimple.ts | 204 +++++++++++++++++- packages/runtime-vapor/src/component.ts | 12 +- packages/runtime-vapor/src/componentProps.ts | 8 +- packages/runtime-vapor/src/index.ts | 1 + playground/src/main.ts | 84 +------- playground/src/style.css | 4 +- 6 files changed, 222 insertions(+), 91 deletions(-) diff --git a/packages/runtime-vapor/src/apiCreateComponentSimple.ts b/packages/runtime-vapor/src/apiCreateComponentSimple.ts index 04ca9b052..c03873402 100644 --- a/packages/runtime-vapor/src/apiCreateComponentSimple.ts +++ b/packages/runtime-vapor/src/apiCreateComponentSimple.ts @@ -2,36 +2,63 @@ import { EffectScope, ReactiveEffect, pauseTracking, - proxyRefs, resetTracking, } from '@vue/reactivity' import { type Component, type ComponentInternalInstance, - createSetupContext, + SetupContext, } 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 { 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 | (() => Record) + +export function createComponentSimple( + component: Component, + rawProps?: RawProps, +): any { 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 ? createSetupContext(instance) : null - const node = setupFn( + const setupContext = setupFn!.length > 1 ? new SetupContext(instance) : null + const node = setupFn!( // TODO __DEV__ ? shallowReadonly(props) : instance.props, + // @ts-expect-error 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() currentInstance = prevInstance resetTracking() + // @ts-expect-error node.__vue__ = instance return node } @@ -40,18 +67,159 @@ let uid = 0 let currentInstance: ComponentInstance | null = null export class ComponentInstance { - type: any + type: Component uid: number = uid++ scope: EffectScope = new EffectScope(true) - props: any - constructor(comp: Component, rawProps: any) { + props: Record + attrs: Record + constructor(comp: Component, rawProps?: RawProps) { this.type = comp // 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 optimization: maybe convert functions into computeds +function resolveSource(source: DynamicPropsSource): Record { + return isFunction(source) ? source() : source +} + +function getPropsProxyHandlers( + comp: Component, +): [ProxyHandler, ProxyHandler] { + 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 + + 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 + + return (comp.__propsHandlers = [propsHandlers, attrsHandlers]) +} + export function renderEffectSimple(fn: () => void): void { const updateFn = () => { fn() @@ -67,3 +235,19 @@ export function renderEffectSimple(fn: () => void): void { // 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) + }, + } +} diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index c91bcb77c..b66fb4930 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -28,14 +28,20 @@ import type { Data } from '@vue/runtime-shared' export type Component = FunctionalComponent | ObjectComponent +type SharedInternalOptions = { + __propsOptions?: NormalizedPropsOptions + __propsHandlers?: [ProxyHandler, ProxyHandler] +} + export type SetupFn = ( props: any, ctx: SetupContext, ) => Block | Data | undefined + export type FunctionalComponent = SetupFn & Omit & { displayName?: string - } + } & SharedInternalOptions export class SetupContext { attrs: Data @@ -96,7 +102,9 @@ export function createSetupContext( } } -export interface ObjectComponent extends ComponentInternalOptions { +export interface ObjectComponent + extends ComponentInternalOptions, + SharedInternalOptions { setup?: SetupFn inheritAttrs?: boolean props?: ComponentPropsOptions diff --git a/packages/runtime-vapor/src/componentProps.ts b/packages/runtime-vapor/src/componentProps.ts index 1fb8d79f6..e59188655 100644 --- a/packages/runtime-vapor/src/componentProps.ts +++ b/packages/runtime-vapor/src/componentProps.ts @@ -257,7 +257,8 @@ function resolvePropValue( } export function normalizePropsOptions(comp: Component): NormalizedPropsOptions { - // TODO: cahching? + const cached = comp.__propsOptions + if (cached) return cached const raw = comp.props 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 } diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index fa84dd514..eb6306232 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -158,6 +158,7 @@ export { createComponent } from './apiCreateComponent' export { createComponentSimple, renderEffectSimple, + createVaporAppSimple, } from './apiCreateComponentSimple' export { createSelector } from './apiCreateSelector' export { setInheritAttrs } from './componentAttrs' diff --git a/playground/src/main.ts b/playground/src/main.ts index 1c4ecdfc1..278c60ee7 100644 --- a/playground/src/main.ts +++ b/playground/src/main.ts @@ -1,79 +1,13 @@ -import { - createComponentSimple, - // createFor, - createVaporApp, - 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('

Vapor

') -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('
') -const Comp = { - vapor: true, - __name: 'Comp', - setup(props: any) { - return (_ctx => { - const n = t0$1() - renderEffectSimple(() => { - n.textContent = props.count + ' / ' + props.plusOne - }) - return n - })() - }, -} +import { createComponentSimple, createVaporAppSimple } from 'vue/vapor' +import List from './list' +import Props from './props' +import './style.css' const s = performance.now() -const app = createVaporApp(App) +const app = createVaporAppSimple({ + setup() { + return [createComponentSimple(Props), createComponentSimple(List)] + }, +}) app.mount('#app') console.log((performance.now() - s).toFixed(2)) diff --git a/playground/src/style.css b/playground/src/style.css index 791b41d45..c6dd2c88f 100644 --- a/playground/src/style.css +++ b/playground/src/style.css @@ -1,3 +1,3 @@ -html { - color-scheme: light dark; +.red { + color: red; }