Skip to content

feat(runtime-vapor): implement proxy instance properties on render #224

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion packages/runtime-vapor/src/apiRender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
import { isArray, isFunction, isObject } from '@vue/shared'
import { fallThroughAttrs } from './componentAttrs'
import { VaporErrorCodes, callWithErrorHandling } from './errorHandling'
import { PublicInstanceProxyHandlers } from './componentPublicInstance'

export const fragmentKey = Symbol(__DEV__ ? `fragmentKey` : ``)

Expand Down Expand Up @@ -65,7 +66,11 @@ export function setupComponent(
}
if (!block && component.render) {
pauseTracking()
block = component.render(instance.setupState)
// 0. create render proxy property access cache
instance.accessCache = Object.create(null)
// 1. create public instance / render proxy
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
block = component.render.call(instance.proxy, instance.proxy)
resetTracking()
}

Expand Down
17 changes: 11 additions & 6 deletions packages/runtime-vapor/src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
} from './apiCreateVaporApp'
import type { Data } from '@vue/runtime-shared'
import { BlockEffectScope } from './blockEffectScope'
import type { ComponentPublicInstance } from './componentPublicInstance'

export type Component = FunctionalComponent | ObjectComponent

Expand Down Expand Up @@ -164,6 +165,7 @@ export interface ComponentInternalInstance {
parent: ComponentInternalInstance | null

provides: Data
accessCache: Data | null
scope: BlockEffectScope
component: Component
comps: Set<ComponentInternalInstance>
Expand All @@ -173,6 +175,7 @@ export interface ComponentInternalInstance {
emitsOptions: ObjectEmitsOptions | null

// state
ctx: Data
setupState: Data
setupContext: SetupContext | null
props: Data
Expand All @@ -184,6 +187,9 @@ export interface ComponentInternalInstance {
// exposed properties via expose()
exposed?: Record<string, any>

// main proxy that serves as the public instance (`this`)
proxy: ComponentPublicInstance | null

attrsProxy?: Data
slotsProxy?: Slots
exposeProxy?: Record<string, any>
Expand Down Expand Up @@ -287,18 +293,20 @@ export function createComponentInstance(
container: null!,

parent,

proxy: null,
scope: null!,
provides: parent ? parent.provides : Object.create(_appContext.provides),
accessCache: null!,
component,
comps: new Set(),

// resolved props and emits options
rawProps: null!, // set later
rawProps: null!,
propsOptions: normalizePropsOptions(component),
emitsOptions: normalizeEmitsOptions(component),

// state
ctx: EMPTY_OBJ,
setupState: EMPTY_OBJ,
setupContext: null,
props: EMPTY_OBJ,
Expand Down Expand Up @@ -357,11 +365,8 @@ export function createComponentInstance(
* @internal
*/
[VaporLifecycleHooks.ERROR_CAPTURED]: null,
/**
* @internal
*/
// [VaporLifecycleHooks.SERVER_PREFETCH]: null,
}
instance.ctx = { _: instance }
instance.scope = new BlockEffectScope(instance, parent && parent.scope)
initProps(instance, rawProps, !isFunction(component), once)
initSlots(instance, slots, dynamicSlots)
Expand Down
231 changes: 231 additions & 0 deletions packages/runtime-vapor/src/componentPublicInstance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
import {
EMPTY_OBJ,
type IfAny,
type Prettify,
extend,
hasOwn,
} from '@vue/shared'
import type { ComponentInternalInstance } from './component'
import { warn } from './warning'
import type { Data } from '@vue/runtime-shared'
import { type ShallowUnwrapRef, shallowReadonly } from '@vue/reactivity'
import type { EmitFn, EmitsOptions } from './componentEmits'
import type { ComponentCustomProperties } from './apiCreateVaporApp'
import type { SlotsType, UnwrapSlotsType } from './componentSlots'

export type InjectToObject<T extends ComponentInjectOptions> =
T extends string[]
? {
[K in T[number]]?: unknown
}
: T extends ObjectInjectOptions
? {
[K in keyof T]?: unknown
}
: never

export type ComponentInjectOptions = string[] | ObjectInjectOptions

type ObjectInjectOptions = Record<
string | symbol,
string | symbol | { from?: string | symbol; default?: unknown }
>

export type ExposedKeys<
T,
Exposed extends string & keyof T,
> = '' extends Exposed ? T : Pick<T, Exposed>

// in templates (as `this` in the render option)
export type ComponentPublicInstance<
P = {}, // props type extracted from props option
B = {}, // raw bindings returned from setup()
E extends EmitsOptions = {},
PublicProps = P,
Defaults = {},
MakeDefaultsOptional extends boolean = false,
I extends ComponentInjectOptions = {},
S extends SlotsType = {},
Exposed extends string = '',
> = {
$: ComponentInternalInstance
$props: MakeDefaultsOptional extends true
? Partial<Defaults> & Omit<Prettify<P> & PublicProps, keyof Defaults>
: Prettify<P> & PublicProps
$attrs: Data
$refs: Data
$slots: UnwrapSlotsType<S>
$parent: ComponentPublicInstance | null
$emit: EmitFn<E>
} & ExposedKeys<
IfAny<P, P, Omit<P, keyof ShallowUnwrapRef<B>>> &
ShallowUnwrapRef<B> &
ComponentCustomProperties &
InjectToObject<I>,
Exposed
>

export let shouldCacheAccess = true

export type PublicPropertiesMap = Record<
string,
(i: ComponentInternalInstance) => any
>

export const publicPropertiesMap: PublicPropertiesMap = extend(
Object.create(null),
{
$: i => i,
$props: i => (__DEV__ ? shallowReadonly(i.props) : i.props),
$attrs: i => (__DEV__ ? shallowReadonly(i.attrs) : i.attrs),
$slots: i => (__DEV__ ? shallowReadonly(i.slots) : i.slots),
$refs: i => (__DEV__ ? shallowReadonly(i.refs) : i.refs),
$emit: i => i.emit,
} as PublicPropertiesMap,
)

enum AccessTypes {
OTHER,
SETUP,
PROPS,
CONTEXT,
}

export interface ComponentRenderContext {
[key: string]: any
_: ComponentInternalInstance
}

const hasSetupBinding = (state: Data, key: string) =>
state !== EMPTY_OBJ && hasOwn(state, key)

export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
get({ _: instance }: ComponentRenderContext, key: string) {
const { ctx, setupState, props, accessCache, appContext } = instance

// for internal formatters to know that this is a Vue instance
if (__DEV__ && key === '__isVue') {
return true
}

let normalizedProps
if (key[0] !== '$') {
const n = accessCache![key]
if (n !== undefined) {
switch (n) {
case AccessTypes.SETUP:
return setupState[key]
case AccessTypes.CONTEXT:
return ctx[key]
case AccessTypes.PROPS:
return props![key]
}
} else if (hasSetupBinding(setupState, key)) {
accessCache![key] = AccessTypes.SETUP
return setupState[key]
} else if (
// only cache other properties when instance has declared (thus stable)
// props
(normalizedProps = instance.propsOptions[0]) &&
hasOwn(normalizedProps, key)
) {
accessCache![key] = AccessTypes.PROPS
return props![key]
} else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
accessCache![key] = AccessTypes.CONTEXT
return ctx[key]
} else if (!__FEATURE_OPTIONS_API__ || shouldCacheAccess) {
accessCache![key] = AccessTypes.OTHER
}
}

const publicGetter = publicPropertiesMap[key]
let globalProperties
// public $xxx properties
if (publicGetter) {
return publicGetter(instance)
} else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
// user may set custom properties to `this` that start with `$`
accessCache![key] = AccessTypes.CONTEXT
return ctx[key]
} else if (
// global properties
((globalProperties = appContext.config.globalProperties),
hasOwn(globalProperties, key))
) {
return globalProperties[key]
}
},

set(
{ _: instance }: ComponentRenderContext,
key: string,
value: any,
): boolean {
const { setupState, ctx } = instance
if (hasSetupBinding(setupState, key)) {
setupState[key] = value
return true
} else if (
__DEV__ &&
setupState.__isScriptSetup &&
hasOwn(setupState, key)
) {
warn(`Cannot mutate <script setup> binding "${key}" from Options API.`)
return false
} else if (hasOwn(instance.props, key)) {
__DEV__ && warn(`Attempting to mutate prop "${key}". Props are readonly.`)
return false
}
if (key[0] === '$' && key.slice(1) in instance) {
__DEV__ &&
warn(
`Attempting to mutate public property "${key}". ` +
`Properties starting with $ are reserved and readonly.`,
)
return false
} else {
if (__DEV__ && key in instance.appContext.config.globalProperties) {
Object.defineProperty(ctx, key, {
enumerable: true,
configurable: true,
value,
})
} else {
ctx[key] = value
}
}
return true
},

has(
{
_: { setupState, accessCache, ctx, appContext, propsOptions },
}: ComponentRenderContext,
key: string,
) {
let normalizedProps
return (
!!accessCache![key] ||
hasSetupBinding(setupState, key) ||
((normalizedProps = propsOptions[0]) && hasOwn(normalizedProps, key)) ||
hasOwn(ctx, key) ||
hasOwn(publicPropertiesMap, key) ||
hasOwn(appContext.config.globalProperties, key)
)
},

defineProperty(
target: ComponentRenderContext,
key: string,
descriptor: PropertyDescriptor,
) {
if (descriptor.get != null) {
// invalidate key cache of a getter based property #5417
target._.accessCache![key] = 0
} else if (hasOwn(descriptor, 'value')) {
this.set!(target, key, descriptor.value, null)
}
return Reflect.defineProperty(target, key, descriptor)
},
}
20 changes: 19 additions & 1 deletion packages/runtime-vapor/src/componentSlots.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type IfAny, isArray, isFunction } from '@vue/shared'
import { type IfAny, type Prettify, isArray, isFunction } from '@vue/shared'
import {
type EffectScope,
effectScope,
Expand All @@ -17,6 +17,24 @@ import { VaporErrorCodes, callWithAsyncErrorHandling } from './errorHandling'

// TODO: SSR

export type UnwrapSlotsType<
S extends SlotsType,
T = NonNullable<S[typeof SlotSymbol]>,
> = [keyof S] extends [never]
? Slots
: Readonly<
Prettify<{
[K in keyof T]: NonNullable<T[K]> extends (...args: any[]) => any
? T[K]
: Slot<T[K]>
}>
>

declare const SlotSymbol: unique symbol
export type SlotsType<T extends Record<string, any> = Record<string, any>> = {
[SlotSymbol]?: T
}

export type Slot<T extends any = any> = (
...args: IfAny<T, any[], [T] | (T extends undefined ? [] : never)>
) => Block
Expand Down
Loading