Skip to content

Commit 8973f68

Browse files
committed
feat(runtime-vapor): implement proxy instance properties on render
1 parent b2259a5 commit 8973f68

File tree

6 files changed

+435
-8
lines changed

6 files changed

+435
-8
lines changed

packages/runtime-vapor/src/apiRender.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
import { isArray, isFunction, isObject } from '@vue/shared'
1818
import { fallThroughAttrs } from './componentAttrs'
1919
import { VaporErrorCodes, callWithErrorHandling } from './errorHandling'
20+
import { PublicInstanceProxyHandlers } from './componentPublicInstance'
2021

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

@@ -65,7 +66,11 @@ export function setupComponent(
6566
}
6667
if (!block && component.render) {
6768
pauseTracking()
68-
block = component.render(instance.setupState)
69+
// 0. create render proxy property access cache
70+
instance.accessCache = Object.create(null)
71+
// 1. create public instance / render proxy
72+
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
73+
block = component.render.call(instance.proxy, instance.proxy)
6974
resetTracking()
7075
}
7176

packages/runtime-vapor/src/component.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
} from './apiCreateVaporApp'
3838
import type { Data } from '@vue/runtime-shared'
3939
import { BlockEffectScope } from './blockEffectScope'
40+
import type { ComponentPublicInstance } from './componentPublicInstance'
4041

4142
export type Component = FunctionalComponent | ObjectComponent
4243

@@ -164,6 +165,7 @@ export interface ComponentInternalInstance {
164165
parent: ComponentInternalInstance | null
165166

166167
provides: Data
168+
accessCache: Data | null
167169
scope: BlockEffectScope
168170
component: Component
169171
comps: Set<ComponentInternalInstance>
@@ -173,6 +175,7 @@ export interface ComponentInternalInstance {
173175
emitsOptions: ObjectEmitsOptions | null
174176

175177
// state
178+
ctx: Data
176179
setupState: Data
177180
setupContext: SetupContext | null
178181
props: Data
@@ -184,6 +187,9 @@ export interface ComponentInternalInstance {
184187
// exposed properties via expose()
185188
exposed?: Record<string, any>
186189

190+
// main proxy that serves as the public instance (`this`)
191+
proxy: ComponentPublicInstance | null
192+
187193
attrsProxy?: Data
188194
slotsProxy?: Slots
189195
exposeProxy?: Record<string, any>
@@ -287,18 +293,20 @@ export function createComponentInstance(
287293
container: null!,
288294

289295
parent,
290-
296+
proxy: null,
291297
scope: null!,
292298
provides: parent ? parent.provides : Object.create(_appContext.provides),
299+
accessCache: null!,
293300
component,
294301
comps: new Set(),
295302

296303
// resolved props and emits options
297-
rawProps: null!, // set later
304+
rawProps: null!,
298305
propsOptions: normalizePropsOptions(component),
299306
emitsOptions: normalizeEmitsOptions(component),
300307

301308
// state
309+
ctx: EMPTY_OBJ,
302310
setupState: EMPTY_OBJ,
303311
setupContext: null,
304312
props: EMPTY_OBJ,
@@ -357,11 +365,8 @@ export function createComponentInstance(
357365
* @internal
358366
*/
359367
[VaporLifecycleHooks.ERROR_CAPTURED]: null,
360-
/**
361-
* @internal
362-
*/
363-
// [VaporLifecycleHooks.SERVER_PREFETCH]: null,
364368
}
369+
instance.ctx = { _: instance }
365370
instance.scope = new BlockEffectScope(instance, parent && parent.scope)
366371
initProps(instance, rawProps, !isFunction(component), once)
367372
initSlots(instance, slots, dynamicSlots)
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
import {
2+
EMPTY_OBJ,
3+
type IfAny,
4+
type Prettify,
5+
extend,
6+
hasOwn,
7+
} from '@vue/shared'
8+
import type { ComponentInternalInstance } from './component'
9+
import { warn } from './warning'
10+
import type { Data } from '@vue/runtime-shared'
11+
import { type ShallowUnwrapRef, shallowReadonly } from '@vue/reactivity'
12+
import type { EmitFn, EmitsOptions } from './componentEmits'
13+
import type { ComponentCustomProperties } from './apiCreateVaporApp'
14+
import type { SlotsType, UnwrapSlotsType } from './componentSlots'
15+
16+
export type InjectToObject<T extends ComponentInjectOptions> =
17+
T extends string[]
18+
? {
19+
[K in T[number]]?: unknown
20+
}
21+
: T extends ObjectInjectOptions
22+
? {
23+
[K in keyof T]?: unknown
24+
}
25+
: never
26+
27+
export type ComponentInjectOptions = string[] | ObjectInjectOptions
28+
29+
type ObjectInjectOptions = Record<
30+
string | symbol,
31+
string | symbol | { from?: string | symbol; default?: unknown }
32+
>
33+
34+
export type ExposedKeys<
35+
T,
36+
Exposed extends string & keyof T,
37+
> = '' extends Exposed ? T : Pick<T, Exposed>
38+
39+
// in templates (as `this` in the render option)
40+
export type ComponentPublicInstance<
41+
P = {}, // props type extracted from props option
42+
B = {}, // raw bindings returned from setup()
43+
E extends EmitsOptions = {},
44+
PublicProps = P,
45+
Defaults = {},
46+
MakeDefaultsOptional extends boolean = false,
47+
I extends ComponentInjectOptions = {},
48+
S extends SlotsType = {},
49+
Exposed extends string = '',
50+
> = {
51+
$: ComponentInternalInstance
52+
$props: MakeDefaultsOptional extends true
53+
? Partial<Defaults> & Omit<Prettify<P> & PublicProps, keyof Defaults>
54+
: Prettify<P> & PublicProps
55+
$attrs: Data
56+
$refs: Data
57+
$slots: UnwrapSlotsType<S>
58+
$parent: ComponentPublicInstance | null
59+
$emit: EmitFn<E>
60+
} & ExposedKeys<
61+
IfAny<P, P, Omit<P, keyof ShallowUnwrapRef<B>>> &
62+
ShallowUnwrapRef<B> &
63+
ComponentCustomProperties &
64+
InjectToObject<I>,
65+
Exposed
66+
>
67+
68+
export let shouldCacheAccess = true
69+
70+
export type PublicPropertiesMap = Record<
71+
string,
72+
(i: ComponentInternalInstance) => any
73+
>
74+
75+
export const publicPropertiesMap: PublicPropertiesMap = extend(
76+
Object.create(null),
77+
{
78+
$: i => i,
79+
$props: i => (__DEV__ ? shallowReadonly(i.props) : i.props),
80+
$attrs: i => (__DEV__ ? shallowReadonly(i.attrs) : i.attrs),
81+
$slots: i => (__DEV__ ? shallowReadonly(i.slots) : i.slots),
82+
$refs: i => (__DEV__ ? shallowReadonly(i.refs) : i.refs),
83+
$emit: i => i.emit,
84+
} as PublicPropertiesMap,
85+
)
86+
87+
enum AccessTypes {
88+
OTHER,
89+
SETUP,
90+
PROPS,
91+
CONTEXT,
92+
}
93+
94+
export interface ComponentRenderContext {
95+
[key: string]: any
96+
_: ComponentInternalInstance
97+
}
98+
99+
const hasSetupBinding = (state: Data, key: string) =>
100+
state !== EMPTY_OBJ && hasOwn(state, key)
101+
102+
export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
103+
get({ _: instance }: ComponentRenderContext, key: string) {
104+
const { ctx, setupState, props, accessCache, appContext } = instance
105+
106+
// for internal formatters to know that this is a Vue instance
107+
if (__DEV__ && key === '__isVue') {
108+
return true
109+
}
110+
111+
let normalizedProps
112+
if (key[0] !== '$') {
113+
const n = accessCache![key]
114+
if (n !== undefined) {
115+
switch (n) {
116+
case AccessTypes.SETUP:
117+
return setupState[key]
118+
case AccessTypes.CONTEXT:
119+
return ctx[key]
120+
case AccessTypes.PROPS:
121+
return props![key]
122+
}
123+
} else if (hasSetupBinding(setupState, key)) {
124+
accessCache![key] = AccessTypes.SETUP
125+
return setupState[key]
126+
} else if (
127+
// only cache other properties when instance has declared (thus stable)
128+
// props
129+
(normalizedProps = instance.propsOptions[0]) &&
130+
hasOwn(normalizedProps, key)
131+
) {
132+
accessCache![key] = AccessTypes.PROPS
133+
return props![key]
134+
} else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
135+
accessCache![key] = AccessTypes.CONTEXT
136+
return ctx[key]
137+
} else if (!__FEATURE_OPTIONS_API__ || shouldCacheAccess) {
138+
accessCache![key] = AccessTypes.OTHER
139+
}
140+
}
141+
142+
const publicGetter = publicPropertiesMap[key]
143+
let globalProperties
144+
// public $xxx properties
145+
if (publicGetter) {
146+
return publicGetter(instance)
147+
} else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
148+
// user may set custom properties to `this` that start with `$`
149+
accessCache![key] = AccessTypes.CONTEXT
150+
return ctx[key]
151+
} else if (
152+
// global properties
153+
((globalProperties = appContext.config.globalProperties),
154+
hasOwn(globalProperties, key))
155+
) {
156+
return globalProperties[key]
157+
}
158+
},
159+
160+
set(
161+
{ _: instance }: ComponentRenderContext,
162+
key: string,
163+
value: any,
164+
): boolean {
165+
const { setupState, ctx } = instance
166+
if (hasSetupBinding(setupState, key)) {
167+
setupState[key] = value
168+
return true
169+
} else if (
170+
__DEV__ &&
171+
setupState.__isScriptSetup &&
172+
hasOwn(setupState, key)
173+
) {
174+
warn(`Cannot mutate <script setup> binding "${key}" from Options API.`)
175+
return false
176+
} else if (hasOwn(instance.props, key)) {
177+
__DEV__ && warn(`Attempting to mutate prop "${key}". Props are readonly.`)
178+
return false
179+
}
180+
if (key[0] === '$' && key.slice(1) in instance) {
181+
__DEV__ &&
182+
warn(
183+
`Attempting to mutate public property "${key}". ` +
184+
`Properties starting with $ are reserved and readonly.`,
185+
)
186+
return false
187+
} else {
188+
if (__DEV__ && key in instance.appContext.config.globalProperties) {
189+
Object.defineProperty(ctx, key, {
190+
enumerable: true,
191+
configurable: true,
192+
value,
193+
})
194+
} else {
195+
ctx[key] = value
196+
}
197+
}
198+
return true
199+
},
200+
201+
has(
202+
{
203+
_: { setupState, accessCache, ctx, appContext, propsOptions },
204+
}: ComponentRenderContext,
205+
key: string,
206+
) {
207+
let normalizedProps
208+
return (
209+
!!accessCache![key] ||
210+
hasSetupBinding(setupState, key) ||
211+
((normalizedProps = propsOptions[0]) && hasOwn(normalizedProps, key)) ||
212+
hasOwn(ctx, key) ||
213+
hasOwn(publicPropertiesMap, key) ||
214+
hasOwn(appContext.config.globalProperties, key)
215+
)
216+
},
217+
218+
defineProperty(
219+
target: ComponentRenderContext,
220+
key: string,
221+
descriptor: PropertyDescriptor,
222+
) {
223+
if (descriptor.get != null) {
224+
// invalidate key cache of a getter based property #5417
225+
target._.accessCache![key] = 0
226+
} else if (hasOwn(descriptor, 'value')) {
227+
this.set!(target, key, descriptor.value, null)
228+
}
229+
return Reflect.defineProperty(target, key, descriptor)
230+
},
231+
}

packages/runtime-vapor/src/componentSlots.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type IfAny, isArray, isFunction } from '@vue/shared'
1+
import { type IfAny, type Prettify, isArray, isFunction } from '@vue/shared'
22
import {
33
type EffectScope,
44
effectScope,
@@ -17,6 +17,24 @@ import { VaporErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
1717

1818
// TODO: SSR
1919

20+
export type UnwrapSlotsType<
21+
S extends SlotsType,
22+
T = NonNullable<S[typeof SlotSymbol]>,
23+
> = [keyof S] extends [never]
24+
? Slots
25+
: Readonly<
26+
Prettify<{
27+
[K in keyof T]: NonNullable<T[K]> extends (...args: any[]) => any
28+
? T[K]
29+
: Slot<T[K]>
30+
}>
31+
>
32+
33+
declare const SlotSymbol: unique symbol
34+
export type SlotsType<T extends Record<string, any> = Record<string, any>> = {
35+
[SlotSymbol]?: T
36+
}
37+
2038
export type Slot<T extends any = any> = (
2139
...args: IfAny<T, any[], [T] | (T extends undefined ? [] : never)>
2240
) => Block

0 commit comments

Comments
 (0)