feat(runtime-core): improve component public instance proxy inspection

This commit is contained in:
Evan You 2020-04-05 18:39:22 -04:00
parent f42d11e8e1
commit 899287ad35
5 changed files with 176 additions and 41 deletions

View File

@ -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

View File

@ -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
)
}
}

View File

@ -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]
}
}
}
}

View File

@ -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
})
})
}

View File

@ -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.