From 23c3a192bcaa7d4ba15fa8c8070f1f0df75d11ce Mon Sep 17 00:00:00 2001 From: Bas van Meurs Date: Wed, 27 May 2020 11:02:13 +0200 Subject: [PATCH 1/8] feat(PublicInstanceProxyHandlers): improve performance by creating scoped functions that return values faster It's much faster when repeatedly accessing variables, but we still need to investigate the memory implications when having a large number of component instances --- packages/runtime-core/src/component.ts | 36 ++- packages/runtime-core/src/componentProxy.ts | 314 +++++++++++--------- 2 files changed, 188 insertions(+), 162 deletions(-) diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 9ae406e3363..0f1290c56db 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -8,11 +8,12 @@ import { } from '@vue/reactivity' import { ComponentPublicInstance, - PublicInstanceProxyHandlers, - RuntimeCompiledPublicInstanceProxyHandlers, createRenderContext, exposePropsOnRenderContext, - exposeSetupStateOnRenderContext + exposeSetupStateOnRenderContext, + createInstanceProxy, + createInstanceWithProxy, + PropGetterFactory } from './componentProxy' import { ComponentPropsOptions, initProps } from './componentProps' import { Slots, initSlots, InternalSlots } from './componentSlots' @@ -178,11 +179,17 @@ export interface ComponentInternalInstance { * @internal */ effects: ReactiveEffect[] | null + /** - * cache for proxy access type to avoid hasOwnProperty calls - * @internal + * Creates a fast instance/property-specific access function to be used in the context proxy. + */ + propGetterFactory: PropGetterFactory | null + + /** + * Provides a quick property accessor in the context proxy. */ - accessCache: Data | null + propGetters: Record any> | null + /** * cache for render function values that rely on _ctx but won't need updates * after initialized (e.g. inline handlers) @@ -343,7 +350,9 @@ export function createComponentInstance( withProxy: null, effects: null, provides: parent ? parent.provides : Object.create(appContext.provides), - accessCache: null!, + propGetterFactory: null, + propGetters: null, + renderCache: [], // state @@ -460,15 +469,13 @@ function setupStatefulComponent( } } } - // 0. create render proxy property access cache - instance.accessCache = {} - // 1. create public instance / render proxy + // 0. create public instance / render proxy // also mark it raw so it's never observed - instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers) + instance.proxy = createInstanceProxy(instance) if (__DEV__) { exposePropsOnRenderContext(instance) } - // 2. call setup() + // 1. call setup() const { setup } = Component if (setup) { const setupContext = (instance.setupContext = @@ -606,10 +613,7 @@ function finishComponentSetup( // proxy used needs a different `has` handler which is more performant and // also only allows a whitelist of globals to fallthrough. if (instance.render._rc) { - instance.withProxy = new Proxy( - instance.ctx, - RuntimeCompiledPublicInstanceProxyHandlers - ) + instance.withProxy = createInstanceWithProxy(instance) } } diff --git a/packages/runtime-core/src/componentProxy.ts b/packages/runtime-core/src/componentProxy.ts index ff895a61969..9a4d6e54cd6 100644 --- a/packages/runtime-core/src/componentProxy.ts +++ b/packages/runtime-core/src/componentProxy.ts @@ -7,7 +7,9 @@ import { UnwrapRef, toRaw, shallowReadonly, - ReactiveFlags + ReactiveFlags, + isRef, + Ref } from '@vue/reactivity' import { ExtractComputedReturns, @@ -104,125 +106,37 @@ const publicPropertiesMap: Record< $watch: __FEATURE_OPTIONS__ ? i => instanceWatch.bind(i) : NOOP } -const enum AccessTypes { - SETUP, - DATA, - PROPS, - CONTEXT, - OTHER -} - export interface ComponentRenderContext { [key: string]: any _: ComponentInternalInstance } -export const PublicInstanceProxyHandlers: ProxyHandler = { - get({ _: instance }: ComponentRenderContext, key: string) { - const { - ctx, - setupState, - data, - props, - accessCache, - type, - appContext - } = instance - - // let @vue/reatvitiy know it should never observe Vue public instances. - if (key === ReactiveFlags.skip) { - return true - } - - // data / props / ctx - // This getter gets called for every property access on the render context - // during render and is a major hotspot. The most expensive part of this - // is the multiple hasOwn() calls. It's much faster to do a simple property - // access on a plain object, so we use an accessCache object (with null - // prototype) to memoize what access type a key corresponds to. - if (key[0] !== '$') { - const n = accessCache![key] - if (n !== undefined) { - switch (n) { - case AccessTypes.SETUP: - return setupState[key] - case AccessTypes.DATA: - return data[key] - case AccessTypes.CONTEXT: - return ctx[key] - case AccessTypes.PROPS: - return props![key] - // default: just fallthrough - } - } else if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) { - accessCache![key] = AccessTypes.SETUP - return setupState[key] - } else if (data !== EMPTY_OBJ && hasOwn(data, key)) { - accessCache![key] = AccessTypes.DATA - return data[key] - } else if ( - // only cache other properties when instance has declared (thus stable) - // props - type.props && - hasOwn(normalizePropsOptions(type.props)[0]!, key) - ) { - accessCache![key] = AccessTypes.PROPS - return props![key] - } else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) { - accessCache![key] = AccessTypes.CONTEXT - return ctx[key] - } else { - accessCache![key] = AccessTypes.OTHER - } +function getPublicInstanceProxyHandlers( + instance: ComponentInternalInstance +): ProxyHandler { + function getPropGetter(key: string): PropGetter { + let fn = instance.propGetters![key] + if (fn) { + return fn + } else { + fn = instance.propGetterFactory!(key) + instance.propGetters![key] = fn + return fn } + } - const publicGetter = publicPropertiesMap[key] - let cssModule, globalProperties - // public $xxx properties - if (publicGetter) { - if (__DEV__ && key === '$attrs') { - markAttrsAccessed() - } - return publicGetter(instance) - } else if ( - // css module (injected by vue-loader) - (cssModule = type.__cssModules) && - (cssModule = cssModule[key]) - ) { - return cssModule - } 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] - } else if ( - __DEV__ && - currentRenderingInstance && - // #1091 avoid internal isRef/isVNode checks on component instance leading - // to infinite warning loop - key.indexOf('__v') !== 0 - ) { - if (data !== EMPTY_OBJ && key[0] === '$' && hasOwn(data, key)) { - warn( - `Property ${JSON.stringify( - key - )} must be accessed via $data because it starts with a reserved ` + - `character and is not proxied on the render context.` - ) - } else { - warn( - `Property ${JSON.stringify(key)} was accessed during render ` + - `but is not defined on instance.` - ) - } + return { + ...PublicInstanceProxyHandlers, + get(c: ComponentRenderContext, key: string) { + return getPropGetter(key)() + }, + has(c: ComponentRenderContext, key: string) { + return getPropGetter(key) !== UNKNOWN_PROP_GETTER } - }, + } +} +const PublicInstanceProxyHandlers: ProxyHandler = { set( { _: instance }: ComponentRenderContext, key: string, @@ -257,27 +171,11 @@ export const PublicInstanceProxyHandlers: ProxyHandler = { value }) } else { + console.log('setting', key) ctx[key] = value } } return true - }, - - has( - { - _: { data, setupState, accessCache, ctx, type, appContext } - }: ComponentRenderContext, - key: string - ) { - return ( - accessCache![key] !== undefined || - (data !== EMPTY_OBJ && hasOwn(data, key)) || - (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) || - (type.props && hasOwn(normalizePropsOptions(type.props)[0]!, key)) || - hasOwn(ctx, key) || - hasOwn(publicPropertiesMap, key) || - hasOwn(appContext.config.globalProperties, key) - ) } } @@ -291,25 +189,30 @@ if (__DEV__ && !__TEST__) { } } -export const RuntimeCompiledPublicInstanceProxyHandlers = { - ...PublicInstanceProxyHandlers, - get(target: ComponentRenderContext, key: string) { - // fast path for unscopables when using `with` block - if ((key as any) === Symbol.unscopables) { - return - } - return PublicInstanceProxyHandlers.get!(target, key, target) - }, - has(_: ComponentRenderContext, key: string) { - const has = key[0] !== '_' && !isGloballyWhitelisted(key) - if (__DEV__ && !has && PublicInstanceProxyHandlers.has!(_, key)) { - warn( - `Property ${JSON.stringify( - key - )} should not start with _ which is a reserved prefix for Vue internals.` - ) +function getRuntimeCompiledPublicInstanceProxyHandlers( + instance: ComponentInternalInstance +): ProxyHandler { + const handlers = getPublicInstanceProxyHandlers(instance) + return { + ...handlers, + get(target: ComponentRenderContext, key: string) { + // fast path for unscopables when using `with` block + if ((key as any) === Symbol.unscopables) { + return + } + return handlers.get!(target, key, target) + }, + has(_: ComponentRenderContext, key: string) { + const has = key[0] !== '_' && !isGloballyWhitelisted(key) + if (__DEV__ && !has && handlers.has!(_, key)) { + warn( + `Property ${JSON.stringify( + key + )} should not start with _ which is a reserved prefix for Vue internals.` + ) + } + return has } - return has } } @@ -352,6 +255,125 @@ export function createRenderContext(instance: ComponentInternalInstance) { return target as ComponentRenderContext } +export function createInstanceProxy(instance: ComponentInternalInstance) { + setupPropGetterFactory(instance) + return new Proxy(instance.ctx, getPublicInstanceProxyHandlers(instance)) +} + +export function createInstanceWithProxy(instance: ComponentInternalInstance) { + setupPropGetterFactory(instance) + return new Proxy( + instance.ctx, + getRuntimeCompiledPublicInstanceProxyHandlers(instance) + ) +} + +function setupPropGetterFactory(instance: ComponentInternalInstance) { + instance.propGetterFactory = createPropGetterFactory(instance) + instance.propGetters = {} +} + +/** + * Creates high-performance getters for context properties. + */ +function createPropGetterFactory( + instance: ComponentInternalInstance +): PropGetterFactory { + // We can't use toRaw on props because we want to track changes. + const props = instance.props + + const propKeys = normalizePropsOptions(instance.type.props) + const cssModules = instance.type.__cssModules + const globalProps = instance.appContext.config.globalProperties + return function(key: string): PropGetter { + const setupState = toRaw(instance.setupState) + + if (key === ReactiveFlags.skip) { + // let @vue/reactivity know it should never observe Vue public instances. + return () => true + } + + if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) { + return createRefPropGetter(setupState[key]) + } else if (instance.data !== EMPTY_OBJ && hasOwn(instance.data, key)) { + return createRefPropGetter(instance.data[key]) + } else if (propKeys && hasOwn(propKeys[0]!, key)) { + // only cache other properties when instance has declared (thus stable) + // props + return createMemberPropGetter(props, key) + } else if (instance.ctx !== EMPTY_OBJ && hasOwn(instance.ctx, key)) { + // Ctx props can be set dynamically so use a member prop getter. + return createMemberPropGetter(instance.ctx, key) + } else if (publicPropertiesMap[key]) { + if (__DEV__ && key === '$attrs') { + markAttrsAccessed() + } + return createSimplePropGetter(publicPropertiesMap[key](instance)) + } else if (cssModules && cssModules[key]) { + return createSimplePropGetter(cssModules[key]) + } else if (hasOwn(globalProps, key)) { + return createMemberPropGetter(globalProps, key) + } else { + if ( + __DEV__ && + currentRenderingInstance && + // #1091 avoid internal isRef/isVNode checks on component instance leading + // to infinite warning loop + key.indexOf('__v') !== 0 + ) { + if ( + instance.data !== EMPTY_OBJ && + key[0] === '$' && + hasOwn(instance.data, key) + ) { + warn( + `Property ${JSON.stringify( + key + )} must be accessed via $data because it starts with a reserved ` + + `character and is not proxied on the render context.` + ) + } else { + warn( + `Property ${JSON.stringify(key)} was accessed during render ` + + `but is not defined on instance.` + ) + } + } + + // Unknown property: ignore. + return UNKNOWN_PROP_GETTER + } + } +} + +function UNKNOWN_PROP_GETTER() { + return undefined +} + +function createRefPropGetter(variable: any): PropGetter { + if (isRef(variable)) { + return function() { + return (variable as Ref).value + } + } else { + return createSimplePropGetter(variable) + } +} + +function createSimplePropGetter(variable: any): PropGetter { + return function() { + return variable + } +} + +function createMemberPropGetter(obj: any, key: string): PropGetter { + // Use this when the property could change value at runtime. + return new Function('o', `return () => o["${key}"]`)(obj) +} + +type PropGetter = () => any +export type PropGetterFactory = (key: string) => PropGetter + // dev only export function exposePropsOnRenderContext( instance: ComponentInternalInstance From 56210b65d13fbe4646fca1b1e67d6c1f117b1491 Mon Sep 17 00:00:00 2001 From: Bas van Meurs Date: Wed, 27 May 2020 15:54:09 +0200 Subject: [PATCH 2/8] fix(componentProxy): found some issues running unit tests --- packages/runtime-core/src/componentProxy.ts | 74 +++++++++++---------- 1 file changed, 39 insertions(+), 35 deletions(-) diff --git a/packages/runtime-core/src/componentProxy.ts b/packages/runtime-core/src/componentProxy.ts index 9a4d6e54cd6..53e09a49580 100644 --- a/packages/runtime-core/src/componentProxy.ts +++ b/packages/runtime-core/src/componentProxy.ts @@ -114,13 +114,43 @@ export interface ComponentRenderContext { function getPublicInstanceProxyHandlers( instance: ComponentInternalInstance ): ProxyHandler { - function getPropGetter(key: string): PropGetter { + function getPropGetter(key: string, mustWarn: boolean): PropGetter { let fn = instance.propGetters![key] if (fn) { return fn } else { fn = instance.propGetterFactory!(key) - instance.propGetters![key] = fn + if (fn !== UNKNOWN_PROP_GETTER) { + // Do not save it because it could be defined later in the ctx. + instance.propGetters![key] = fn + } else { + if ( + __DEV__ && + mustWarn && + currentRenderingInstance && + // #1091 avoid internal isRef/isVNode checks on component instance leading + // to infinite warning loop + key.indexOf('__v') !== 0 + ) { + if ( + instance.data !== EMPTY_OBJ && + key[0] === '$' && + hasOwn(instance.data, key) + ) { + warn( + `Property ${JSON.stringify( + key + )} must be accessed via $data because it starts with a reserved ` + + `character and is not proxied on the render context.` + ) + } else { + warn( + `Property ${JSON.stringify(key)} was accessed during render ` + + `but is not defined on instance.` + ) + } + } + } return fn } } @@ -128,10 +158,10 @@ function getPublicInstanceProxyHandlers( return { ...PublicInstanceProxyHandlers, get(c: ComponentRenderContext, key: string) { - return getPropGetter(key)() + return getPropGetter(key, true)() }, has(c: ComponentRenderContext, key: string) { - return getPropGetter(key) !== UNKNOWN_PROP_GETTER + return getPropGetter(key, false) !== UNKNOWN_PROP_GETTER } } } @@ -296,50 +326,24 @@ function createPropGetterFactory( if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) { return createRefPropGetter(setupState[key]) } else if (instance.data !== EMPTY_OBJ && hasOwn(instance.data, key)) { - return createRefPropGetter(instance.data[key]) - } else if (propKeys && hasOwn(propKeys[0]!, key)) { + return createMemberPropGetter(instance.data, key) + } else if (propKeys && propKeys[0] && hasOwn(propKeys[0]!, key)) { // only cache other properties when instance has declared (thus stable) // props return createMemberPropGetter(props, key) - } else if (instance.ctx !== EMPTY_OBJ && hasOwn(instance.ctx, key)) { - // Ctx props can be set dynamically so use a member prop getter. - return createMemberPropGetter(instance.ctx, key) } else if (publicPropertiesMap[key]) { if (__DEV__ && key === '$attrs') { markAttrsAccessed() } return createSimplePropGetter(publicPropertiesMap[key](instance)) + } else if (instance.ctx !== EMPTY_OBJ && hasOwn(instance.ctx, key)) { + // Ctx props can be set dynamically so use a member prop getter. + return createMemberPropGetter(instance.ctx, key) } else if (cssModules && cssModules[key]) { return createSimplePropGetter(cssModules[key]) } else if (hasOwn(globalProps, key)) { return createMemberPropGetter(globalProps, key) } else { - if ( - __DEV__ && - currentRenderingInstance && - // #1091 avoid internal isRef/isVNode checks on component instance leading - // to infinite warning loop - key.indexOf('__v') !== 0 - ) { - if ( - instance.data !== EMPTY_OBJ && - key[0] === '$' && - hasOwn(instance.data, key) - ) { - warn( - `Property ${JSON.stringify( - key - )} must be accessed via $data because it starts with a reserved ` + - `character and is not proxied on the render context.` - ) - } else { - warn( - `Property ${JSON.stringify(key)} was accessed during render ` + - `but is not defined on instance.` - ) - } - } - // Unknown property: ignore. return UNKNOWN_PROP_GETTER } From 0ce64c7d60c18c0ed8e939cf53f4458097a1c879 Mon Sep 17 00:00:00 2001 From: Bas van Meurs Date: Wed, 27 May 2020 16:24:33 +0200 Subject: [PATCH 3/8] feat(componentProxy): improve typing --- packages/runtime-core/src/component.ts | 5 +++-- packages/runtime-core/src/componentProxy.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 0f1290c56db..37cf4e13876 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -13,7 +13,8 @@ import { exposeSetupStateOnRenderContext, createInstanceProxy, createInstanceWithProxy, - PropGetterFactory + PropGetterFactory, + PropGetter } from './componentProxy' import { ComponentPropsOptions, initProps } from './componentProps' import { Slots, initSlots, InternalSlots } from './componentSlots' @@ -188,7 +189,7 @@ export interface ComponentInternalInstance { /** * Provides a quick property accessor in the context proxy. */ - propGetters: Record any> | null + propGetters: Record | null /** * cache for render function values that rely on _ctx but won't need updates diff --git a/packages/runtime-core/src/componentProxy.ts b/packages/runtime-core/src/componentProxy.ts index 53e09a49580..5c1cffe6ea5 100644 --- a/packages/runtime-core/src/componentProxy.ts +++ b/packages/runtime-core/src/componentProxy.ts @@ -375,7 +375,7 @@ function createMemberPropGetter(obj: any, key: string): PropGetter { return new Function('o', `return () => o["${key}"]`)(obj) } -type PropGetter = () => any +export type PropGetter = () => any export type PropGetterFactory = (key: string) => PropGetter // dev only From ea2f01588700abc4de4e7cac59ed4643db8dd63b Mon Sep 17 00:00:00 2001 From: Bas van Meurs Date: Thu, 28 May 2020 10:15:39 +0200 Subject: [PATCH 4/8] chore(componentProxy): remove console.log --- packages/runtime-core/src/componentProxy.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/runtime-core/src/componentProxy.ts b/packages/runtime-core/src/componentProxy.ts index 5c1cffe6ea5..4d5d5d07f6c 100644 --- a/packages/runtime-core/src/componentProxy.ts +++ b/packages/runtime-core/src/componentProxy.ts @@ -201,7 +201,6 @@ const PublicInstanceProxyHandlers: ProxyHandler = { value }) } else { - console.log('setting', key) ctx[key] = value } } From 8b31ee95313caa84df4e08b643432ae1f7190404 Mon Sep 17 00:00:00 2001 From: Bas van Meurs Date: Thu, 28 May 2020 20:44:12 +0200 Subject: [PATCH 5/8] feat(componentProxy): do not store a scoped PropGetterFactory per component instance Because it does not impact proxy get performance and it does decrease memory consumption and improve instance creation performance. --- packages/runtime-core/src/component.ts | 7 -- packages/runtime-core/src/componentProxy.ts | 87 +++++++++++---------- 2 files changed, 44 insertions(+), 50 deletions(-) diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 37cf4e13876..77454f5f4e9 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -13,7 +13,6 @@ import { exposeSetupStateOnRenderContext, createInstanceProxy, createInstanceWithProxy, - PropGetterFactory, PropGetter } from './componentProxy' import { ComponentPropsOptions, initProps } from './componentProps' @@ -181,11 +180,6 @@ export interface ComponentInternalInstance { */ effects: ReactiveEffect[] | null - /** - * Creates a fast instance/property-specific access function to be used in the context proxy. - */ - propGetterFactory: PropGetterFactory | null - /** * Provides a quick property accessor in the context proxy. */ @@ -351,7 +345,6 @@ export function createComponentInstance( withProxy: null, effects: null, provides: parent ? parent.provides : Object.create(appContext.provides), - propGetterFactory: null, propGetters: null, renderCache: [], diff --git a/packages/runtime-core/src/componentProxy.ts b/packages/runtime-core/src/componentProxy.ts index 4d5d5d07f6c..7eeb04d0db4 100644 --- a/packages/runtime-core/src/componentProxy.ts +++ b/packages/runtime-core/src/componentProxy.ts @@ -119,7 +119,7 @@ function getPublicInstanceProxyHandlers( if (fn) { return fn } else { - fn = instance.propGetterFactory!(key) + fn = getGetterForProxyKey(instance, key) if (fn !== UNKNOWN_PROP_GETTER) { // Do not save it because it could be defined later in the ctx. instance.propGetters![key] = fn @@ -285,67 +285,68 @@ export function createRenderContext(instance: ComponentInternalInstance) { } export function createInstanceProxy(instance: ComponentInternalInstance) { - setupPropGetterFactory(instance) + setupPropGetters(instance) return new Proxy(instance.ctx, getPublicInstanceProxyHandlers(instance)) } export function createInstanceWithProxy(instance: ComponentInternalInstance) { - setupPropGetterFactory(instance) + setupPropGetters(instance) return new Proxy( instance.ctx, getRuntimeCompiledPublicInstanceProxyHandlers(instance) ) } -function setupPropGetterFactory(instance: ComponentInternalInstance) { - instance.propGetterFactory = createPropGetterFactory(instance) +function setupPropGetters(instance: ComponentInternalInstance) { instance.propGetters = {} } /** - * Creates high-performance getters for context properties. + * Returns a getter function for an instance proxy property. */ -function createPropGetterFactory( - instance: ComponentInternalInstance -): PropGetterFactory { - // We can't use toRaw on props because we want to track changes. - const props = instance.props +function getGetterForProxyKey( + instance: ComponentInternalInstance, + key: string +): PropGetter { + const setupState = toRaw(instance.setupState) - const propKeys = normalizePropsOptions(instance.type.props) - const cssModules = instance.type.__cssModules - const globalProps = instance.appContext.config.globalProperties - return function(key: string): PropGetter { - const setupState = toRaw(instance.setupState) + if (key === ReactiveFlags.skip) { + // let @vue/reactivity know it should never observe Vue public instances. + return () => true + } - if (key === ReactiveFlags.skip) { - // let @vue/reactivity know it should never observe Vue public instances. - return () => true - } + if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) { + return createRefPropGetter(setupState[key]) + } else if (instance.data !== EMPTY_OBJ && hasOwn(instance.data, key)) { + return createMemberPropGetter(instance.data, key) + } - if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) { - return createRefPropGetter(setupState[key]) - } else if (instance.data !== EMPTY_OBJ && hasOwn(instance.data, key)) { - return createMemberPropGetter(instance.data, key) - } else if (propKeys && propKeys[0] && hasOwn(propKeys[0]!, key)) { - // only cache other properties when instance has declared (thus stable) - // props - return createMemberPropGetter(props, key) - } else if (publicPropertiesMap[key]) { - if (__DEV__ && key === '$attrs') { - markAttrsAccessed() - } - return createSimplePropGetter(publicPropertiesMap[key](instance)) - } else if (instance.ctx !== EMPTY_OBJ && hasOwn(instance.ctx, key)) { - // Ctx props can be set dynamically so use a member prop getter. - return createMemberPropGetter(instance.ctx, key) - } else if (cssModules && cssModules[key]) { - return createSimplePropGetter(cssModules[key]) - } else if (hasOwn(globalProps, key)) { - return createMemberPropGetter(globalProps, key) - } else { - // Unknown property: ignore. - return UNKNOWN_PROP_GETTER + const propKeys = normalizePropsOptions(instance.type.props) + if (propKeys && propKeys[0] && hasOwn(propKeys[0]!, key)) { + // only cache other properties when instance has declared (thus stable) + // props + return createMemberPropGetter(instance.props, key) + } else if (publicPropertiesMap[key]) { + if (__DEV__ && key === '$attrs') { + markAttrsAccessed() } + return createSimplePropGetter(publicPropertiesMap[key](instance)) + } else if (instance.ctx !== EMPTY_OBJ && hasOwn(instance.ctx, key)) { + // Ctx props can be set dynamically so use a member prop getter. + return createMemberPropGetter(instance.ctx, key) + } + + const cssModules = instance.type.__cssModules + if (cssModules && cssModules[key]) { + return createSimplePropGetter(cssModules[key]) + } else if (hasOwn(instance.appContext.config.globalProperties, key)) { + return createMemberPropGetter( + instance.appContext.config.globalProperties, + key + ) + } else { + // Unknown property: ignore. + return UNKNOWN_PROP_GETTER } } From 0b7d473b34e24356d5690cc9900775bb7208037f Mon Sep 17 00:00:00 2001 From: Bas van Meurs Date: Thu, 28 May 2020 21:52:23 +0200 Subject: [PATCH 6/8] feat(componentProxy): do not create new functions for every instance, rather reuse them per component type Reusing ensures that memory usage is limited, while only slightly impacting performance. --- packages/runtime-core/src/componentProxy.ts | 104 +++++++++++--------- 1 file changed, 60 insertions(+), 44 deletions(-) diff --git a/packages/runtime-core/src/componentProxy.ts b/packages/runtime-core/src/componentProxy.ts index 7eeb04d0db4..4104ee93bf2 100644 --- a/packages/runtime-core/src/componentProxy.ts +++ b/packages/runtime-core/src/componentProxy.ts @@ -1,4 +1,4 @@ -import { ComponentInternalInstance, Data } from './component' +import { Component, ComponentInternalInstance, Data } from './component' import { nextTick, queueJob } from './scheduler' import { instanceWatch } from './apiWatch' import { EMPTY_OBJ, hasOwn, isGloballyWhitelisted, NOOP } from '@vue/shared' @@ -8,8 +8,7 @@ import { toRaw, shallowReadonly, ReactiveFlags, - isRef, - Ref + unref } from '@vue/reactivity' import { ExtractComputedReturns, @@ -114,14 +113,16 @@ export interface ComponentRenderContext { function getPublicInstanceProxyHandlers( instance: ComponentInternalInstance ): ProxyHandler { - function getPropGetter(key: string, mustWarn: boolean): PropGetter { - let fn = instance.propGetters![key] + function getPropGetter( + key: string, + mustWarn: boolean + ): PropGetter | undefined { + let fn: PropGetter | undefined = instance.propGetters![key] if (fn) { return fn } else { fn = getGetterForProxyKey(instance, key) - if (fn !== UNKNOWN_PROP_GETTER) { - // Do not save it because it could be defined later in the ctx. + if (fn) { instance.propGetters![key] = fn } else { if ( @@ -158,10 +159,12 @@ function getPublicInstanceProxyHandlers( return { ...PublicInstanceProxyHandlers, get(c: ComponentRenderContext, key: string) { - return getPropGetter(key, true)() + const propGetter = getPropGetter(key, true) + return propGetter ? propGetter.f(propGetter.a) : undefined }, has(c: ComponentRenderContext, key: string) { - return getPropGetter(key, false) !== UNKNOWN_PROP_GETTER + const propGetter = getPropGetter(key, false) + return propGetter !== undefined } } } @@ -301,83 +304,96 @@ function setupPropGetters(instance: ComponentInternalInstance) { instance.propGetters = {} } +export type PropGetter = { f: (arg: T) => any; a: T } + /** * Returns a getter function for an instance proxy property. */ function getGetterForProxyKey( instance: ComponentInternalInstance, key: string -): PropGetter { +): PropGetter | undefined { const setupState = toRaw(instance.setupState) if (key === ReactiveFlags.skip) { // let @vue/reactivity know it should never observe Vue public instances. - return () => true + return { f: () => true, a: undefined } } if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) { - return createRefPropGetter(setupState[key]) + return { f: unref, a: setupState[key] } } else if (instance.data !== EMPTY_OBJ && hasOwn(instance.data, key)) { - return createMemberPropGetter(instance.data, key) + return { + f: createComponentMemberPropGetter(instance.type, key), + a: instance.data + } } const propKeys = normalizePropsOptions(instance.type.props) if (propKeys && propKeys[0] && hasOwn(propKeys[0]!, key)) { // only cache other properties when instance has declared (thus stable) // props - return createMemberPropGetter(instance.props, key) + return { + f: createComponentMemberPropGetter(instance.type, key), + a: instance.props + } } else if (publicPropertiesMap[key]) { if (__DEV__ && key === '$attrs') { markAttrsAccessed() } - return createSimplePropGetter(publicPropertiesMap[key](instance)) + return { f: publicPropertiesMap[key], a: instance } } else if (instance.ctx !== EMPTY_OBJ && hasOwn(instance.ctx, key)) { // Ctx props can be set dynamically so use a member prop getter. - return createMemberPropGetter(instance.ctx, key) + return { + f: createComponentMemberPropGetter(instance.type, key), + a: instance.ctx + } } const cssModules = instance.type.__cssModules if (cssModules && cssModules[key]) { - return createSimplePropGetter(cssModules[key]) + return { f: getArg, a: cssModules[key] } } else if (hasOwn(instance.appContext.config.globalProperties, key)) { - return createMemberPropGetter( - instance.appContext.config.globalProperties, - key - ) + return { + f: createComponentMemberPropGetter(instance.type, key), + a: instance.appContext.config.globalProperties + } } else { // Unknown property: ignore. - return UNKNOWN_PROP_GETTER + return undefined } } -function UNKNOWN_PROP_GETTER() { - return undefined +function getArg(a: T): T { + return a } -function createRefPropGetter(variable: any): PropGetter { - if (isRef(variable)) { - return function() { - return (variable as Ref).value - } - } else { - return createSimplePropGetter(variable) +// Member property getters are reused to limit memory usage. +// However, we want a different one per component type as that makes it more +// likely that the object param is always of the same shape, allow optimizations. +type ComponentMemberPropGetter = (o: object) => any +const componentMemberPropGetters = new WeakMap< + Component, + Record +>() +function createComponentMemberPropGetter( + component: Component, + key: string +): ComponentMemberPropGetter { + let getters = componentMemberPropGetters.get(component) + if (!getters) { + getters = {} + componentMemberPropGetters.set(component, getters) } -} - -function createSimplePropGetter(variable: any): PropGetter { - return function() { - return variable + if (!getters[key]) { + getters[key] = new Function( + 'o', + `return o["${key}"]` + ) as ComponentMemberPropGetter } + return getters[key] } -function createMemberPropGetter(obj: any, key: string): PropGetter { - // Use this when the property could change value at runtime. - return new Function('o', `return () => o["${key}"]`)(obj) -} - -export type PropGetter = () => any -export type PropGetterFactory = (key: string) => PropGetter - // dev only export function exposePropsOnRenderContext( instance: ComponentInternalInstance From d232d2e4e9c11687216f5adb37d4c52d70546091 Mon Sep 17 00:00:00 2001 From: Bas van Meurs Date: Thu, 28 May 2020 22:05:22 +0200 Subject: [PATCH 7/8] refactor(componentProxy): improve structure and move getPropGetter to global scope --- packages/runtime-core/src/componentProxy.ts | 96 +++++++++++---------- 1 file changed, 52 insertions(+), 44 deletions(-) diff --git a/packages/runtime-core/src/componentProxy.ts b/packages/runtime-core/src/componentProxy.ts index 4104ee93bf2..60f10ee0939 100644 --- a/packages/runtime-core/src/componentProxy.ts +++ b/packages/runtime-core/src/componentProxy.ts @@ -110,65 +110,73 @@ export interface ComponentRenderContext { _: ComponentInternalInstance } -function getPublicInstanceProxyHandlers( - instance: ComponentInternalInstance -): ProxyHandler { - function getPropGetter( - key: string, - mustWarn: boolean - ): PropGetter | undefined { - let fn: PropGetter | undefined = instance.propGetters![key] +function getPropGetter( + instance: ComponentInternalInstance, + key: string +): PropGetter | undefined { + let fn: PropGetter | undefined = instance.propGetters![key] + if (fn) { + return fn + } else { + fn = getGetterForProxyKey(instance, key) if (fn) { - return fn - } else { - fn = getGetterForProxyKey(instance, key) - if (fn) { - instance.propGetters![key] = fn - } else { - if ( - __DEV__ && - mustWarn && - currentRenderingInstance && - // #1091 avoid internal isRef/isVNode checks on component instance leading - // to infinite warning loop - key.indexOf('__v') !== 0 - ) { - if ( - instance.data !== EMPTY_OBJ && - key[0] === '$' && - hasOwn(instance.data, key) - ) { - warn( - `Property ${JSON.stringify( - key - )} must be accessed via $data because it starts with a reserved ` + - `character and is not proxied on the render context.` - ) - } else { - warn( - `Property ${JSON.stringify(key)} was accessed during render ` + - `but is not defined on instance.` - ) - } - } - } - return fn + instance.propGetters![key] = fn } + return fn } +} +function getPublicInstanceProxyHandlers( + instance: ComponentInternalInstance +): ProxyHandler { return { ...PublicInstanceProxyHandlers, get(c: ComponentRenderContext, key: string) { - const propGetter = getPropGetter(key, true) + const propGetter = getPropGetter(instance, key) + if (__DEV__) { + if (!propGetter) { + warnForUnknownProxyPropertyAccess(instance, key) + } + } return propGetter ? propGetter.f(propGetter.a) : undefined }, has(c: ComponentRenderContext, key: string) { - const propGetter = getPropGetter(key, false) + const propGetter = getPropGetter(instance, key) return propGetter !== undefined } } } +function warnForUnknownProxyPropertyAccess( + instance: ComponentInternalInstance, + key: string +) { + if ( + currentRenderingInstance && + // #1091 avoid internal isRef/isVNode checks on component instance leading + // to infinite warning loop + key.indexOf('__v') !== 0 + ) { + if ( + instance.data !== EMPTY_OBJ && + key[0] === '$' && + hasOwn(instance.data, key) + ) { + warn( + `Property ${JSON.stringify( + key + )} must be accessed via $data because it starts with a reserved ` + + `character and is not proxied on the render context.` + ) + } else { + warn( + `Property ${JSON.stringify(key)} was accessed during render ` + + `but is not defined on instance.` + ) + } + } +} + const PublicInstanceProxyHandlers: ProxyHandler = { set( { _: instance }: ComponentRenderContext, From facda6228ba145cb02428ebb5476fd6d41c9cb37 Mon Sep 17 00:00:00 2001 From: Bas van Meurs Date: Thu, 28 May 2020 22:35:30 +0200 Subject: [PATCH 8/8] refactor(componentProxy): add comment why a new get/has proxy handler is created for every component instance --- packages/runtime-core/src/componentProxy.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/runtime-core/src/componentProxy.ts b/packages/runtime-core/src/componentProxy.ts index 60f10ee0939..288b08d5004 100644 --- a/packages/runtime-core/src/componentProxy.ts +++ b/packages/runtime-core/src/componentProxy.ts @@ -129,6 +129,8 @@ function getPropGetter( function getPublicInstanceProxyHandlers( instance: ComponentInternalInstance ): ProxyHandler { + // We create a new get and has function per instance, because it allows us to + // scope the instance object directly making the (total) 'get' time ~25% faster. return { ...PublicInstanceProxyHandlers, get(c: ComponentRenderContext, key: string) {