]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: props handling
authorEvan You <evan@vuejs.org>
Mon, 2 Dec 2024 12:35:45 +0000 (20:35 +0800)
committerEvan You <evan@vuejs.org>
Mon, 2 Dec 2024 12:35:45 +0000 (20:35 +0800)
packages/runtime-vapor/src/apiCreateComponentSimple.ts
packages/runtime-vapor/src/component.ts
packages/runtime-vapor/src/componentProps.ts
packages/runtime-vapor/src/index.ts
playground/src/main.ts
playground/src/style.css

index 04ca9b0527a1bfe330a9c3e57c8a5758e0456042..c03873402b46a5a77d57f006c8eeaa7b475bb4d9 100644 (file)
@@ -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<string, any> | (() => Record<string, any>)
+
+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<string, any>
+  attrs: Record<string, any>
+  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<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 {
   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)
+    },
+  }
+}
index c91bcb77ce2ac15ace1cb06b6f8e988212d5cac8..b66fb49302ae869051159ea0aa90565bdfb560ce 100644 (file)
@@ -28,14 +28,20 @@ import type { Data } from '@vue/runtime-shared'
 
 export type Component = FunctionalComponent | ObjectComponent
 
+type SharedInternalOptions = {
+  __propsOptions?: NormalizedPropsOptions
+  __propsHandlers?: [ProxyHandler<any>, ProxyHandler<any>]
+}
+
 export type SetupFn = (
   props: any,
   ctx: SetupContext,
 ) => Block | Data | undefined
+
 export type FunctionalComponent = SetupFn &
   Omit<ObjectComponent, 'setup'> & {
     displayName?: string
-  }
+  } & SharedInternalOptions
 
 export class SetupContext<E = EmitsOptions> {
   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
index 1fb8d79f681e5ec9996f50cf21e5e122585131de..e5918865542ee1a5827f8b913d860072db9524fc 100644 (file)
@@ -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
 }
 
index fa84dd51485e132ee933b714005457e717259725..eb6306232e1ccc24d25978607d12b4f45467501f 100644 (file)
@@ -158,6 +158,7 @@ export { createComponent } from './apiCreateComponent'
 export {
   createComponentSimple,
   renderEffectSimple,
+  createVaporAppSimple,
 } from './apiCreateComponentSimple'
 export { createSelector } from './apiCreateSelector'
 export { setInheritAttrs } from './componentAttrs'
index 1c4ecdfc1ffdbf7894d3025955b051437fbc9d6d..278c60ee7b04c1929bb691f315ae7f74377f9220 100644 (file)
@@ -1,79 +1,13 @@
-import {
-  createComponentSimple,
-  // createFor,
-  createVaporApp,
-  delegate,
-  delegateEvents,
-  ref,
-  renderEffectSimple,
-  template,
-} from 'vue/vapor'
+import { createComponentSimple, createVaporAppSimple } from 'vue/vapor'
+import List from './list'
+import Props from './props'
+import './style.css'
 
-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',
+const s = performance.now()
+const app = createVaporAppSimple({
   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
-    ]
+    return [createComponentSimple(Props), createComponentSimple(List)]
   },
-}
-
-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 app = createVaporApp(App)
+})
 app.mount('#app')
 console.log((performance.now() - s).toFixed(2))
index 791b41d4581e5128918459256eedeb2b482d5569..c6dd2c88fabe35196d739f2d6621ea3a48877a7a 100644 (file)
@@ -1,3 +1,3 @@
-html {
-  color-scheme: light dark;
+.red {
+  color: red;
 }