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,
|
||||
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)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -158,6 +158,7 @@ export { createComponent } from './apiCreateComponent'
|
|||
export {
|
||||
createComponentSimple,
|
||||
renderEffectSimple,
|
||||
createVaporAppSimple,
|
||||
} from './apiCreateComponentSimple'
|
||||
export { createSelector } from './apiCreateSelector'
|
||||
export { setInheritAttrs } from './componentAttrs'
|
||||
|
|
|
@ -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('<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
|
||||
})()
|
||||
},
|
||||
}
|
||||
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))
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
html {
|
||||
color-scheme: light dark;
|
||||
.red {
|
||||
color: red;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue