mirror of https://github.com/vuejs/core.git
wip: props handling
This commit is contained in:
parent
0acafc7b4d
commit
41c18ef272
|
@ -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)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
html {
|
.red {
|
||||||
color-scheme: light dark;
|
color: red;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue