feat(runtime-core): improve component public instance proxy inspection
This commit is contained in:
parent
f42d11e8e1
commit
899287ad35
@ -108,8 +108,10 @@ describe('component: proxy', () => {
|
||||
expect(instanceProxy.$attrs).toBe(instance!.attrs)
|
||||
expect(instanceProxy.$slots).toBe(instance!.slots)
|
||||
expect(instanceProxy.$refs).toBe(instance!.refs)
|
||||
expect(instanceProxy.$parent).toBe(instance!.parent)
|
||||
expect(instanceProxy.$root).toBe(instance!.root)
|
||||
expect(instanceProxy.$parent).toBe(
|
||||
instance!.parent && instance!.parent.proxy
|
||||
)
|
||||
expect(instanceProxy.$root).toBe(instance!.root.proxy)
|
||||
expect(instanceProxy.$emit).toBe(instance!.emit)
|
||||
expect(instanceProxy.$el).toBe(instance!.vnode.el)
|
||||
expect(instanceProxy.$options).toBe(instance!.type)
|
||||
@ -174,6 +176,14 @@ describe('component: proxy', () => {
|
||||
// set non-existent (goes into sink)
|
||||
instanceProxy.baz = 1
|
||||
expect('baz' in instanceProxy).toBe(true)
|
||||
|
||||
// dev mode ownKeys check for console inspection
|
||||
expect(Object.keys(instanceProxy)).toMatchObject([
|
||||
'msg',
|
||||
'bar',
|
||||
'foo',
|
||||
'baz'
|
||||
])
|
||||
})
|
||||
|
||||
// #864
|
||||
|
@ -7,9 +7,13 @@ import {
|
||||
resetTracking
|
||||
} from '@vue/reactivity'
|
||||
import {
|
||||
PublicInstanceProxyHandlers,
|
||||
ComponentPublicInstance,
|
||||
runtimeCompiledRenderProxyHandlers
|
||||
ComponentPublicProxyTarget,
|
||||
PublicInstanceProxyHandlers,
|
||||
RuntimeCompiledPublicInstanceProxyHandlers,
|
||||
createDevProxyTarget,
|
||||
exposePropsOnDevProxyTarget,
|
||||
exposeRenderContextOnDevProxyTarget
|
||||
} from './componentProxy'
|
||||
import { ComponentPropsOptions, resolveProps } from './componentProps'
|
||||
import { Slots, resolveSlots } from './componentSlots'
|
||||
@ -139,6 +143,7 @@ export interface ComponentInternalInstance {
|
||||
attrs: Data
|
||||
slots: Slots
|
||||
proxy: ComponentPublicInstance | null
|
||||
proxyTarget: ComponentPublicProxyTarget
|
||||
// alternative proxy used only for runtime-compiled render functions using
|
||||
// `with` block
|
||||
withProxy: ComponentPublicInstance | null
|
||||
@ -195,12 +200,13 @@ export function createComponentInstance(
|
||||
parent,
|
||||
appContext,
|
||||
type: vnode.type as Component,
|
||||
root: null!, // set later so it can point to itself
|
||||
root: null!, // to be immediately set
|
||||
next: null,
|
||||
subTree: null!, // will be set synchronously right after creation
|
||||
update: null!, // will be set synchronously right after creation
|
||||
render: null,
|
||||
proxy: null,
|
||||
proxyTarget: null!, // to be immediately set
|
||||
withProxy: null,
|
||||
propsProxy: null,
|
||||
setupContext: null,
|
||||
@ -250,6 +256,11 @@ export function createComponentInstance(
|
||||
ec: null,
|
||||
emit: null as any // to be set immediately
|
||||
}
|
||||
if (__DEV__) {
|
||||
instance.proxyTarget = createDevProxyTarget(instance)
|
||||
} else {
|
||||
instance.proxyTarget = { _: instance }
|
||||
}
|
||||
instance.root = parent ? parent.root : instance
|
||||
instance.emit = emit.bind(null, instance)
|
||||
return instance
|
||||
@ -325,7 +336,10 @@ function setupStatefulComponent(
|
||||
// 0. create render proxy property access cache
|
||||
instance.accessCache = {}
|
||||
// 1. create public instance / render proxy
|
||||
instance.proxy = new Proxy(instance, PublicInstanceProxyHandlers)
|
||||
instance.proxy = new Proxy(instance.proxyTarget, PublicInstanceProxyHandlers)
|
||||
if (__DEV__) {
|
||||
exposePropsOnDevProxyTarget(instance)
|
||||
}
|
||||
// 2. create props proxy
|
||||
// the propsProxy is a reactive AND readonly proxy to the actual props.
|
||||
// it will be updated in resolveProps() on updates before render
|
||||
@ -353,7 +367,7 @@ function setupStatefulComponent(
|
||||
if (isSSR) {
|
||||
// return the promise so server-renderer can wait on it
|
||||
return setupResult.then((resolvedResult: unknown) => {
|
||||
handleSetupResult(instance, resolvedResult, parentSuspense, isSSR)
|
||||
handleSetupResult(instance, resolvedResult, isSSR)
|
||||
})
|
||||
} else if (__FEATURE_SUSPENSE__) {
|
||||
// async setup returned Promise.
|
||||
@ -366,7 +380,7 @@ function setupStatefulComponent(
|
||||
)
|
||||
}
|
||||
} else {
|
||||
handleSetupResult(instance, setupResult, parentSuspense, isSSR)
|
||||
handleSetupResult(instance, setupResult, isSSR)
|
||||
}
|
||||
} else {
|
||||
finishComponentSetup(instance, isSSR)
|
||||
@ -376,7 +390,6 @@ function setupStatefulComponent(
|
||||
export function handleSetupResult(
|
||||
instance: ComponentInternalInstance,
|
||||
setupResult: unknown,
|
||||
parentSuspense: SuspenseBoundary | null,
|
||||
isSSR: boolean
|
||||
) {
|
||||
if (isFunction(setupResult)) {
|
||||
@ -392,6 +405,9 @@ export function handleSetupResult(
|
||||
// setup returned bindings.
|
||||
// assuming a render function compiled from template is present.
|
||||
instance.renderContext = reactive(setupResult)
|
||||
if (__DEV__) {
|
||||
exposeRenderContextOnDevProxyTarget(instance)
|
||||
}
|
||||
} else if (__DEV__ && setupResult !== undefined) {
|
||||
warn(
|
||||
`setup() should return an object. Received: ${
|
||||
@ -460,8 +476,8 @@ function finishComponentSetup(
|
||||
// also only allows a whitelist of globals to fallthrough.
|
||||
if (instance.render._rc) {
|
||||
instance.withProxy = new Proxy(
|
||||
instance,
|
||||
runtimeCompiledRenderProxyHandlers
|
||||
instance.proxyTarget,
|
||||
RuntimeCompiledPublicInstanceProxyHandlers
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -38,9 +38,14 @@ import {
|
||||
import {
|
||||
reactive,
|
||||
ComputedGetter,
|
||||
WritableComputedOptions
|
||||
WritableComputedOptions,
|
||||
ComputedRef
|
||||
} from '@vue/reactivity'
|
||||
import { ComponentObjectPropsOptions, ExtractPropTypes } from './componentProps'
|
||||
import {
|
||||
ComponentObjectPropsOptions,
|
||||
ExtractPropTypes,
|
||||
normalizePropsOptions
|
||||
} from './componentProps'
|
||||
import { EmitsOptions } from './componentEmits'
|
||||
import { Directive } from './directives'
|
||||
import { ComponentPublicInstance } from './componentProxy'
|
||||
@ -239,6 +244,7 @@ export function applyOptions(
|
||||
options: ComponentOptions,
|
||||
asMixin: boolean = false
|
||||
) {
|
||||
const proxyTarget = instance.proxyTarget
|
||||
const ctx = instance.proxy!
|
||||
const {
|
||||
// composition
|
||||
@ -277,7 +283,7 @@ export function applyOptions(
|
||||
|
||||
const globalMixins = instance.appContext.mixins
|
||||
// call it only during dev
|
||||
const checkDuplicateProperties = __DEV__ ? createDuplicateChecker() : null
|
||||
|
||||
// applyOptions is called non-as-mixin once per instance
|
||||
if (!asMixin) {
|
||||
callSyncHook('beforeCreate', options, ctx, globalMixins)
|
||||
@ -293,8 +299,10 @@ export function applyOptions(
|
||||
applyMixins(instance, mixins)
|
||||
}
|
||||
|
||||
const checkDuplicateProperties = __DEV__ ? createDuplicateChecker() : null
|
||||
|
||||
if (__DEV__ && propsOptions) {
|
||||
for (const key in propsOptions) {
|
||||
for (const key in normalizePropsOptions(propsOptions)[0]) {
|
||||
checkDuplicateProperties!(OptionTypes.PROPS, key)
|
||||
}
|
||||
}
|
||||
@ -314,6 +322,7 @@ export function applyOptions(
|
||||
if (__DEV__) {
|
||||
for (const key in data) {
|
||||
checkDuplicateProperties!(OptionTypes.DATA, key)
|
||||
if (!(key in proxyTarget)) proxyTarget[key] = data[key]
|
||||
}
|
||||
}
|
||||
instance.data = reactive(data)
|
||||
@ -326,9 +335,6 @@ export function applyOptions(
|
||||
if (computedOptions) {
|
||||
for (const key in computedOptions) {
|
||||
const opt = (computedOptions as ComputedOptions)[key]
|
||||
|
||||
__DEV__ && checkDuplicateProperties!(OptionTypes.COMPUTED, key)
|
||||
|
||||
if (isFunction(opt)) {
|
||||
renderContext[key] = computed(opt.bind(ctx, ctx))
|
||||
} else {
|
||||
@ -350,6 +356,15 @@ export function applyOptions(
|
||||
warn(`Computed property "${key}" has no getter.`)
|
||||
}
|
||||
}
|
||||
if (__DEV__) {
|
||||
checkDuplicateProperties!(OptionTypes.COMPUTED, key)
|
||||
if (renderContext[key] && !(key in proxyTarget)) {
|
||||
Object.defineProperty(proxyTarget, key, {
|
||||
enumerable: true,
|
||||
get: () => (renderContext[key] as ComputedRef).value
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -357,8 +372,13 @@ export function applyOptions(
|
||||
for (const key in methods) {
|
||||
const methodHandler = (methods as MethodOptions)[key]
|
||||
if (isFunction(methodHandler)) {
|
||||
__DEV__ && checkDuplicateProperties!(OptionTypes.METHODS, key)
|
||||
renderContext[key] = methodHandler.bind(ctx)
|
||||
if (__DEV__) {
|
||||
checkDuplicateProperties!(OptionTypes.METHODS, key)
|
||||
if (!(key in proxyTarget)) {
|
||||
proxyTarget[key] = renderContext[key]
|
||||
}
|
||||
}
|
||||
} else if (__DEV__) {
|
||||
warn(
|
||||
`Method "${key}" has type "${typeof methodHandler}" in the component definition. ` +
|
||||
@ -387,18 +407,24 @@ export function applyOptions(
|
||||
if (isArray(injectOptions)) {
|
||||
for (let i = 0; i < injectOptions.length; i++) {
|
||||
const key = injectOptions[i]
|
||||
__DEV__ && checkDuplicateProperties!(OptionTypes.INJECT, key)
|
||||
renderContext[key] = inject(key)
|
||||
if (__DEV__) {
|
||||
checkDuplicateProperties!(OptionTypes.INJECT, key)
|
||||
proxyTarget[key] = renderContext[key]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const key in injectOptions) {
|
||||
__DEV__ && checkDuplicateProperties!(OptionTypes.INJECT, key)
|
||||
const opt = injectOptions[key]
|
||||
if (isObject(opt)) {
|
||||
renderContext[key] = inject(opt.from, opt.default)
|
||||
} else {
|
||||
renderContext[key] = inject(opt)
|
||||
}
|
||||
if (__DEV__) {
|
||||
checkDuplicateProperties!(OptionTypes.INJECT, key)
|
||||
proxyTarget[key] = renderContext[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { ComponentInternalInstance, Data } from './component'
|
||||
import { nextTick, queueJob } from './scheduler'
|
||||
import { instanceWatch } from './apiWatch'
|
||||
import { EMPTY_OBJ, hasOwn, isGloballyWhitelisted, NOOP } from '@vue/shared'
|
||||
import { ReactiveEffect, UnwrapRef } from '@vue/reactivity'
|
||||
import { ReactiveEffect, UnwrapRef, toRaw } from '@vue/reactivity'
|
||||
import {
|
||||
ExtractComputedReturns,
|
||||
ComponentOptionsBase,
|
||||
@ -61,8 +61,8 @@ const publicPropertiesMap: Record<
|
||||
$attrs: i => i.attrs,
|
||||
$slots: i => i.slots,
|
||||
$refs: i => i.refs,
|
||||
$parent: i => i.parent,
|
||||
$root: i => i.root,
|
||||
$parent: i => i.parent && i.parent.proxy,
|
||||
$root: i => i.root && i.root.proxy,
|
||||
$emit: i => i.emit,
|
||||
$options: i => (__FEATURE_OPTIONS__ ? resolveMergedOptions(i) : i.type),
|
||||
$forceUpdate: i => () => queueJob(i.update),
|
||||
@ -77,8 +77,13 @@ const enum AccessTypes {
|
||||
OTHER
|
||||
}
|
||||
|
||||
export interface ComponentPublicProxyTarget {
|
||||
[key: string]: any
|
||||
_: ComponentInternalInstance
|
||||
}
|
||||
|
||||
export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
|
||||
get(target: ComponentInternalInstance, key: string) {
|
||||
get({ _: instance }: ComponentPublicProxyTarget, key: string) {
|
||||
const {
|
||||
renderContext,
|
||||
data,
|
||||
@ -87,7 +92,7 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
|
||||
type,
|
||||
sink,
|
||||
appContext
|
||||
} = target
|
||||
} = instance
|
||||
|
||||
// data / props / renderContext
|
||||
// This getter gets called for every property access on the render context
|
||||
@ -133,7 +138,7 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
|
||||
if (__DEV__ && key === '$attrs') {
|
||||
markAttrsAccessed()
|
||||
}
|
||||
return publicGetter(target)
|
||||
return publicGetter(instance)
|
||||
} else if (hasOwn(sink, key)) {
|
||||
return sink[key]
|
||||
} else if (
|
||||
@ -154,53 +159,131 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
|
||||
}
|
||||
},
|
||||
|
||||
has(target: ComponentInternalInstance, key: string) {
|
||||
const { data, accessCache, renderContext, type, sink } = target
|
||||
has(
|
||||
{
|
||||
_: { data, accessCache, renderContext, type, sink }
|
||||
}: ComponentPublicProxyTarget,
|
||||
key: string
|
||||
) {
|
||||
return (
|
||||
accessCache![key] !== undefined ||
|
||||
(data !== EMPTY_OBJ && hasOwn(data, key)) ||
|
||||
hasOwn(renderContext, key) ||
|
||||
(type.props && hasOwn(type.props, key)) ||
|
||||
(type.props && hasOwn(normalizePropsOptions(type.props)[0], key)) ||
|
||||
hasOwn(publicPropertiesMap, key) ||
|
||||
hasOwn(sink, key)
|
||||
)
|
||||
},
|
||||
|
||||
set(target: ComponentInternalInstance, key: string, value: any): boolean {
|
||||
const { data, renderContext } = target
|
||||
set(
|
||||
{ _: instance }: ComponentPublicProxyTarget,
|
||||
key: string,
|
||||
value: any
|
||||
): boolean {
|
||||
const { data, renderContext } = instance
|
||||
if (data !== EMPTY_OBJ && hasOwn(data, key)) {
|
||||
data[key] = value
|
||||
} else if (hasOwn(renderContext, key)) {
|
||||
renderContext[key] = value
|
||||
} else if (key[0] === '$' && key.slice(1) in target) {
|
||||
} else if (key[0] === '$' && key.slice(1) in instance) {
|
||||
__DEV__ &&
|
||||
warn(
|
||||
`Attempting to mutate public property "${key}". ` +
|
||||
`Properties starting with $ are reserved and readonly.`,
|
||||
target
|
||||
instance
|
||||
)
|
||||
return false
|
||||
} else if (key in target.props) {
|
||||
} else if (key in instance.props) {
|
||||
__DEV__ &&
|
||||
warn(`Attempting to mutate prop "${key}". Props are readonly.`, target)
|
||||
warn(
|
||||
`Attempting to mutate prop "${key}". Props are readonly.`,
|
||||
instance
|
||||
)
|
||||
return false
|
||||
} else {
|
||||
target.sink[key] = value
|
||||
instance.sink[key] = value
|
||||
if (__DEV__) {
|
||||
instance.proxyTarget[key] = value
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export const runtimeCompiledRenderProxyHandlers = {
|
||||
export const RuntimeCompiledPublicInstanceProxyHandlers = {
|
||||
...PublicInstanceProxyHandlers,
|
||||
get(target: ComponentInternalInstance, key: string) {
|
||||
get(target: ComponentPublicProxyTarget, key: string) {
|
||||
// fast path for unscopables when using `with` block
|
||||
if ((key as any) === Symbol.unscopables) {
|
||||
return
|
||||
}
|
||||
return PublicInstanceProxyHandlers.get!(target, key, target)
|
||||
},
|
||||
has(_target: ComponentInternalInstance, key: string) {
|
||||
has(_: ComponentPublicProxyTarget, key: string) {
|
||||
return key[0] !== '_' && !isGloballyWhitelisted(key)
|
||||
}
|
||||
}
|
||||
|
||||
// In dev mode, the proxy target exposes the same properties as seen on `this`
|
||||
// for easier console inspection. In prod mode it will be an empty object so
|
||||
// these properties definitions can be skipped.
|
||||
export function createDevProxyTarget(instance: ComponentInternalInstance) {
|
||||
const target: Record<string, any> = {}
|
||||
|
||||
// expose internal instance for proxy handlers
|
||||
Object.defineProperty(target, `_`, {
|
||||
get: () => instance
|
||||
})
|
||||
|
||||
// expose public properties
|
||||
Object.keys(publicPropertiesMap).forEach(key => {
|
||||
Object.defineProperty(target, key, {
|
||||
get: () => publicPropertiesMap[key](instance)
|
||||
})
|
||||
})
|
||||
|
||||
// expose global properties
|
||||
const { globalProperties } = instance.appContext.config
|
||||
Object.keys(globalProperties).forEach(key => {
|
||||
Object.defineProperty(target, key, {
|
||||
get: () => globalProperties[key]
|
||||
})
|
||||
})
|
||||
|
||||
return target as ComponentPublicProxyTarget
|
||||
}
|
||||
|
||||
export function exposePropsOnDevProxyTarget(
|
||||
instance: ComponentInternalInstance
|
||||
) {
|
||||
const {
|
||||
proxyTarget,
|
||||
type: { props: propsOptions }
|
||||
} = instance
|
||||
if (propsOptions) {
|
||||
Object.keys(normalizePropsOptions(propsOptions)[0]).forEach(key => {
|
||||
Object.defineProperty(proxyTarget, key, {
|
||||
enumerable: true,
|
||||
get: () => instance.props[key],
|
||||
// intercepted by the proxy so no need for implementation,
|
||||
// but needed to prevent set errors
|
||||
set: NOOP
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function exposeRenderContextOnDevProxyTarget(
|
||||
instance: ComponentInternalInstance
|
||||
) {
|
||||
const { proxyTarget, renderContext } = instance
|
||||
Object.keys(toRaw(renderContext)).forEach(key => {
|
||||
Object.defineProperty(proxyTarget, key, {
|
||||
enumerable: true,
|
||||
get: () => renderContext[key],
|
||||
// intercepted by the proxy so no need for implementation,
|
||||
// but needed to prevent set errors
|
||||
set: NOOP
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -419,7 +419,7 @@ function createSuspenseBoundary(
|
||||
if (__DEV__) {
|
||||
pushWarningContext(vnode)
|
||||
}
|
||||
handleSetupResult(instance, asyncSetupResult, suspense, false)
|
||||
handleSetupResult(instance, asyncSetupResult, false)
|
||||
if (hydratedEl) {
|
||||
// vnode may have been replaced if an update happened before the
|
||||
// async dep is reoslved.
|
||||
|
Loading…
Reference in New Issue
Block a user