From 370fc820cc16c441db52d73d498cede5e851c443 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 16 Apr 2020 12:49:50 -0400 Subject: [PATCH] refactor(runtime-core): refactor instance public proxy context object --- .../__tests__/componentProxy.spec.ts | 6 +- packages/runtime-core/src/component.ts | 48 +++--- packages/runtime-core/src/componentOptions.ts | 137 ++++++++---------- packages/runtime-core/src/componentProxy.ts | 78 +++++----- .../runtime-core/src/components/KeepAlive.ts | 10 +- packages/runtime-core/src/renderer.ts | 6 +- 6 files changed, 131 insertions(+), 154 deletions(-) diff --git a/packages/runtime-core/__tests__/componentProxy.spec.ts b/packages/runtime-core/__tests__/componentProxy.spec.ts index 8b88ca9b..d5d46c25 100644 --- a/packages/runtime-core/__tests__/componentProxy.spec.ts +++ b/packages/runtime-core/__tests__/componentProxy.spec.ts @@ -116,7 +116,7 @@ describe('component: proxy', () => { render(h(Comp), nodeOps.createElement('div')) instanceProxy.foo = 1 expect(instanceProxy.foo).toBe(1) - expect(instance!.proxyTarget.foo).toBe(1) + expect(instance!.ctx.foo).toBe(1) }) test('globalProperties', () => { @@ -141,7 +141,7 @@ describe('component: proxy', () => { // set should overwrite globalProperties with local instanceProxy.foo = 2 // expect(instanceProxy.foo).toBe(2) - expect(instance!.proxyTarget.foo).toBe(2) + expect(instance!.ctx.foo).toBe(2) // should not affect global expect(app.config.globalProperties.foo).toBe(1) }) @@ -177,7 +177,7 @@ describe('component: proxy', () => { expect('msg' in instanceProxy).toBe(true) // data expect('foo' in instanceProxy).toBe(true) - // renderContext + // ctx expect('bar' in instanceProxy).toBe(true) // public properties expect('$el' in instanceProxy).toBe(true) diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 53ee3e41..e4893b98 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -8,12 +8,11 @@ import { } from '@vue/reactivity' import { ComponentPublicInstance, - ComponentPublicProxyTarget, PublicInstanceProxyHandlers, RuntimeCompiledPublicInstanceProxyHandlers, - createDevProxyTarget, - exposePropsOnDevProxyTarget, - exposeSetupStateOnDevProxyTarget + createRenderContext, + exposePropsOnRenderContext, + exposeSetupStateOnRenderContext } from './componentProxy' import { ComponentPropsOptions, initProps } from './componentProps' import { Slots, initSlots, InternalSlots } from './componentSlots' @@ -136,13 +135,23 @@ export interface ComponentInternalInstance { components: Record directives: Record - // the rest are only for stateful components - renderContext: Data + // the rest are only for stateful components --------------------------------- + + // main proxy that serves as the public instance (`this`) + proxy: ComponentPublicInstance | null + // alternative proxy used only for runtime-compiled render functions using + // `with` block + withProxy: ComponentPublicInstance | null + // This is the target for the public instance proxy. It also holds properties + // injected by user options (computed, methods etc.) and user-attached + // custom properties (via `this.x = ...`) + ctx: Data + + // internal state data: Data props: Data attrs: Data slots: InternalSlots - proxy: ComponentPublicInstance | null refs: Data emit: EmitFn @@ -150,16 +159,6 @@ export interface ComponentInternalInstance { setupState: Data setupContext: SetupContext | null - // The target object for the public instance proxy. In dev mode, we also - // define getters for all known instance properties on it so it can be - // properly inspected in the console. These getters are skipped in prod mode - // for performance. In addition, any user attached properties - // (via `this.x = ...`) are also stored on this object. - proxyTarget: ComponentPublicProxyTarget - // alternative proxy used only for runtime-compiled render functions using - // `with` block - withProxy: ComponentPublicInstance | null - // suspense related suspense: SuspenseBoundary | null asyncDep: Promise | null @@ -211,7 +210,6 @@ export function createComponentInstance( update: null!, // will be set synchronously right after creation render: null, proxy: null, - proxyTarget: null!, // to be immediately set withProxy: null, effects: null, provides: parent ? parent.provides : Object.create(appContext.provides), @@ -219,7 +217,7 @@ export function createComponentInstance( renderCache: [], // state - renderContext: EMPTY_OBJ, + ctx: EMPTY_OBJ, data: EMPTY_OBJ, props: EMPTY_OBJ, attrs: EMPTY_OBJ, @@ -258,9 +256,9 @@ export function createComponentInstance( emit: null as any // to be set immediately } if (__DEV__) { - instance.proxyTarget = createDevProxyTarget(instance) + instance.ctx = createRenderContext(instance) } else { - instance.proxyTarget = { _: instance } + instance.ctx = { _: instance } } instance.root = parent ? parent.root : instance instance.emit = emit.bind(null, instance) @@ -335,9 +333,9 @@ function setupStatefulComponent( // 0. create render proxy property access cache instance.accessCache = {} // 1. create public instance / render proxy - instance.proxy = new Proxy(instance.proxyTarget, PublicInstanceProxyHandlers) + instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers) if (__DEV__) { - exposePropsOnDevProxyTarget(instance) + exposePropsOnRenderContext(instance) } // 2. call setup() const { setup } = Component @@ -399,7 +397,7 @@ export function handleSetupResult( // assuming a render function compiled from template is present. instance.setupState = reactive(setupResult) if (__DEV__) { - exposeSetupStateOnDevProxyTarget(instance) + exposeSetupStateOnRenderContext(instance) } } else if (__DEV__ && setupResult !== undefined) { warn( @@ -469,7 +467,7 @@ function finishComponentSetup( // also only allows a whitelist of globals to fallthrough. if (instance.render._rc) { instance.withProxy = new Proxy( - instance.proxyTarget, + instance.ctx, RuntimeCompiledPublicInstanceProxyHandlers ) } diff --git a/packages/runtime-core/src/componentOptions.ts b/packages/runtime-core/src/componentOptions.ts index 20c99a71..e8fbfad4 100644 --- a/packages/runtime-core/src/componentOptions.ts +++ b/packages/runtime-core/src/componentOptions.ts @@ -39,9 +39,7 @@ import { import { reactive, ComputedGetter, - WritableComputedOptions, - ComputedRef, - toRaw + WritableComputedOptions } from '@vue/reactivity' import { ComponentObjectPropsOptions, @@ -246,8 +244,7 @@ export function applyOptions( options: ComponentOptions, asMixin: boolean = false ) { - const proxyTarget = instance.proxyTarget - const ctx = instance.proxy! + const publicThis = instance.proxy! const { // composition mixins, @@ -277,19 +274,13 @@ export function applyOptions( errorCaptured } = options - const renderContext = toRaw( - instance.renderContext === EMPTY_OBJ && - (computedOptions || methods || watchOptions || injectOptions) - ? (instance.renderContext = reactive({})) - : instance.renderContext - ) - + const ctx = instance.ctx const globalMixins = instance.appContext.mixins // call it only during dev // applyOptions is called non-as-mixin once per instance if (!asMixin) { - callSyncHook('beforeCreate', options, ctx, globalMixins) + callSyncHook('beforeCreate', options, publicThis, globalMixins) // global mixins are applied first applyMixins(instance, globalMixins) } @@ -318,7 +309,7 @@ export function applyOptions( `Plain object usage is no longer supported.` ) } - const data = dataOptions.call(ctx, ctx) + const data = dataOptions.call(publicThis, publicThis) if (__DEV__ && isPromise(data)) { warn( `data() returned a Promise - note data() cannot be async; If you ` + @@ -332,7 +323,13 @@ export function applyOptions( if (__DEV__) { for (const key in data) { checkDuplicateProperties!(OptionTypes.DATA, key) - if (!(key in proxyTarget)) proxyTarget[key] = data[key] + // expose data on ctx during dev + Object.defineProperty(ctx, key, { + configurable: true, + enumerable: true, + get: () => data[key], + set: NOOP + }) } } instance.data = reactive(data) @@ -345,37 +342,36 @@ export function applyOptions( if (computedOptions) { for (const key in computedOptions) { const opt = (computedOptions as ComputedOptions)[key] - if (isFunction(opt)) { - renderContext[key] = computed(opt.bind(ctx, ctx)) - } else { - const { get, set } = opt - if (isFunction(get)) { - renderContext[key] = computed({ - get: get.bind(ctx, ctx), - set: isFunction(set) - ? set.bind(ctx) - : __DEV__ - ? () => { - warn( - `Write operation failed: computed property "${key}" is readonly.` - ) - } - : NOOP - }) - } else if (__DEV__) { - warn(`Computed property "${key}" has no getter.`) - } + const get = isFunction(opt) + ? opt.bind(publicThis, publicThis) + : isFunction(opt.get) + ? opt.get.bind(publicThis, publicThis) + : NOOP + if (__DEV__ && get === NOOP) { + warn(`Computed property "${key}" has no getter.`) } + const set = + !isFunction(opt) && isFunction(opt.set) + ? opt.set.bind(publicThis) + : __DEV__ + ? () => { + warn( + `Write operation failed: computed property "${key}" is readonly.` + ) + } + : NOOP + const c = computed({ + get, + set + }) + Object.defineProperty(ctx, key, { + enumerable: true, + configurable: true, + get: () => c.value, + set: v => (c.value = v) + }) if (__DEV__) { checkDuplicateProperties!(OptionTypes.COMPUTED, key) - if (renderContext[key] && !(key in proxyTarget)) { - Object.defineProperty(proxyTarget, key, { - enumerable: true, - configurable: true, - get: () => (renderContext[key] as ComputedRef).value, - set: NOOP - }) - } } } } @@ -384,12 +380,9 @@ export function applyOptions( for (const key in methods) { const methodHandler = (methods as MethodOptions)[key] if (isFunction(methodHandler)) { - renderContext[key] = methodHandler.bind(ctx) + ctx[key] = methodHandler.bind(publicThis) if (__DEV__) { checkDuplicateProperties!(OptionTypes.METHODS, key) - if (!(key in proxyTarget)) { - proxyTarget[key] = renderContext[key] - } } } else if (__DEV__) { warn( @@ -402,13 +395,13 @@ export function applyOptions( if (watchOptions) { for (const key in watchOptions) { - createWatcher(watchOptions[key], renderContext, ctx, key) + createWatcher(watchOptions[key], ctx, publicThis, key) } } if (provideOptions) { const provides = isFunction(provideOptions) - ? provideOptions.call(ctx) + ? provideOptions.call(publicThis) : provideOptions for (const key in provides) { provide(key, provides[key]) @@ -419,23 +412,21 @@ export function applyOptions( if (isArray(injectOptions)) { for (let i = 0; i < injectOptions.length; i++) { const key = injectOptions[i] - renderContext[key] = inject(key) + ctx[key] = inject(key) if (__DEV__) { checkDuplicateProperties!(OptionTypes.INJECT, key) - proxyTarget[key] = renderContext[key] } } } else { for (const key in injectOptions) { const opt = injectOptions[key] if (isObject(opt)) { - renderContext[key] = inject(opt.from, opt.default) + ctx[key] = inject(opt.from, opt.default) } else { - renderContext[key] = inject(opt) + ctx[key] = inject(opt) } if (__DEV__) { checkDuplicateProperties!(OptionTypes.INJECT, key) - proxyTarget[key] = renderContext[key] } } } @@ -451,40 +442,40 @@ export function applyOptions( // lifecycle options if (!asMixin) { - callSyncHook('created', options, ctx, globalMixins) + callSyncHook('created', options, publicThis, globalMixins) } if (beforeMount) { - onBeforeMount(beforeMount.bind(ctx)) + onBeforeMount(beforeMount.bind(publicThis)) } if (mounted) { - onMounted(mounted.bind(ctx)) + onMounted(mounted.bind(publicThis)) } if (beforeUpdate) { - onBeforeUpdate(beforeUpdate.bind(ctx)) + onBeforeUpdate(beforeUpdate.bind(publicThis)) } if (updated) { - onUpdated(updated.bind(ctx)) + onUpdated(updated.bind(publicThis)) } if (activated) { - onActivated(activated.bind(ctx)) + onActivated(activated.bind(publicThis)) } if (deactivated) { - onDeactivated(deactivated.bind(ctx)) + onDeactivated(deactivated.bind(publicThis)) } if (errorCaptured) { - onErrorCaptured(errorCaptured.bind(ctx)) + onErrorCaptured(errorCaptured.bind(publicThis)) } if (renderTracked) { - onRenderTracked(renderTracked.bind(ctx)) + onRenderTracked(renderTracked.bind(publicThis)) } if (renderTriggered) { - onRenderTriggered(renderTriggered.bind(ctx)) + onRenderTriggered(renderTriggered.bind(publicThis)) } if (beforeUnmount) { - onBeforeUnmount(beforeUnmount.bind(ctx)) + onBeforeUnmount(beforeUnmount.bind(publicThis)) } if (unmounted) { - onUnmounted(unmounted.bind(ctx)) + onUnmounted(unmounted.bind(publicThis)) } } @@ -533,25 +524,25 @@ function applyMixins( function createWatcher( raw: ComponentWatchOptionItem, - renderContext: Data, - ctx: ComponentPublicInstance, + ctx: Data, + publicThis: ComponentPublicInstance, key: string ) { - const getter = () => (ctx as Data)[key] + const getter = () => (publicThis as Data)[key] if (isString(raw)) { - const handler = renderContext[raw] + const handler = ctx[raw] if (isFunction(handler)) { watch(getter, handler as WatchCallback) } else if (__DEV__) { warn(`Invalid watch handler specified by key "${raw}"`, handler) } } else if (isFunction(raw)) { - watch(getter, raw.bind(ctx)) + watch(getter, raw.bind(publicThis)) } else if (isObject(raw)) { if (isArray(raw)) { - raw.forEach(r => createWatcher(r, renderContext, ctx, key)) + raw.forEach(r => createWatcher(r, ctx, publicThis, key)) } else { - watch(getter, raw.handler.bind(ctx), raw) + watch(getter, raw.handler.bind(publicThis), raw) } } else if (__DEV__) { warn(`Invalid watch option: "${key}"`) diff --git a/packages/runtime-core/src/componentProxy.ts b/packages/runtime-core/src/componentProxy.ts index d8f97c4b..03044d07 100644 --- a/packages/runtime-core/src/componentProxy.ts +++ b/packages/runtime-core/src/componentProxy.ts @@ -83,25 +83,24 @@ const enum AccessTypes { OTHER } -export interface ComponentPublicProxyTarget { +export interface ComponentRenderContext { [key: string]: any _: ComponentInternalInstance } export const PublicInstanceProxyHandlers: ProxyHandler = { - get({ _: instance }: ComponentPublicProxyTarget, key: string) { + get({ _: instance }: ComponentRenderContext, key: string) { const { - renderContext, + ctx, setupState, data, props, accessCache, type, - proxyTarget, appContext } = instance - // data / props / renderContext + // 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 @@ -116,7 +115,7 @@ export const PublicInstanceProxyHandlers: ProxyHandler = { case AccessTypes.DATA: return data[key] case AccessTypes.CONTEXT: - return renderContext[key] + return ctx[key] case AccessTypes.PROPS: return props![key] // default: just fallthrough @@ -135,31 +134,30 @@ export const PublicInstanceProxyHandlers: ProxyHandler = { ) { accessCache![key] = AccessTypes.PROPS return props![key] - } else if (renderContext !== EMPTY_OBJ && hasOwn(renderContext, key)) { + } else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) { accessCache![key] = AccessTypes.CONTEXT - return renderContext[key] + return ctx[key] } else { accessCache![key] = AccessTypes.OTHER } } - // public $xxx properties & - // user-attached properties (falls through to proxyTarget) const publicGetter = publicPropertiesMap[key] let cssModule, globalProperties + // public $xxx properties if (publicGetter) { if (__DEV__ && key === '$attrs') { markAttrsAccessed() } return publicGetter(instance) - } else if (hasOwn(proxyTarget, key)) { - return proxyTarget[key] } else if ( + // css module (injected by vue-loader) (cssModule = type.__cssModules) && (cssModule = cssModule[key]) ) { return cssModule } else if ( + // global properties ((globalProperties = appContext.config.globalProperties), hasOwn(globalProperties, key)) ) { @@ -173,11 +171,11 @@ export const PublicInstanceProxyHandlers: ProxyHandler = { }, set( - { _: instance }: ComponentPublicProxyTarget, + { _: instance }: ComponentRenderContext, key: string, value: any ): boolean { - const { data, setupState, renderContext } = instance + const { data, setupState, ctx } = instance if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) { setupState[key] = value } else if (data !== EMPTY_OBJ && hasOwn(data, key)) { @@ -189,9 +187,8 @@ export const PublicInstanceProxyHandlers: ProxyHandler = { instance ) return false - } else if (hasOwn(renderContext, key)) { - renderContext[key] = value - } else if (key[0] === '$' && key.slice(1) in instance) { + } + if (key[0] === '$' && key.slice(1) in instance) { __DEV__ && warn( `Attempting to mutate public property "${key}". ` + @@ -201,13 +198,13 @@ export const PublicInstanceProxyHandlers: ProxyHandler = { return false } else { if (__DEV__ && key in instance.appContext.config.globalProperties) { - Object.defineProperty(instance.proxyTarget, key, { - configurable: true, + Object.defineProperty(ctx, key, { enumerable: true, + configurable: true, value }) } else { - instance.proxyTarget[key] = value + ctx[key] = value } } return true @@ -215,16 +212,8 @@ export const PublicInstanceProxyHandlers: ProxyHandler = { has( { - _: { - data, - setupState, - accessCache, - renderContext, - type, - proxyTarget, - appContext - } - }: ComponentPublicProxyTarget, + _: { data, setupState, accessCache, ctx, type, appContext } + }: ComponentRenderContext, key: string ) { return ( @@ -232,18 +221,15 @@ export const PublicInstanceProxyHandlers: ProxyHandler = { (data !== EMPTY_OBJ && hasOwn(data, key)) || (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) || (type.props && hasOwn(normalizePropsOptions(type.props)[0]!, key)) || - hasOwn(renderContext, key) || + hasOwn(ctx, key) || hasOwn(publicPropertiesMap, key) || - hasOwn(proxyTarget, key) || hasOwn(appContext.config.globalProperties, key) ) } } if (__DEV__ && !__TEST__) { - PublicInstanceProxyHandlers.ownKeys = ( - target: ComponentPublicProxyTarget - ) => { + PublicInstanceProxyHandlers.ownKeys = (target: ComponentRenderContext) => { warn( `Avoid app logic that relies on enumerating keys on a component instance. ` + `The keys will be empty in production mode to avoid performance overhead.` @@ -254,14 +240,14 @@ if (__DEV__ && !__TEST__) { export const RuntimeCompiledPublicInstanceProxyHandlers = { ...PublicInstanceProxyHandlers, - get(target: ComponentPublicProxyTarget, key: string) { + 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(_: ComponentPublicProxyTarget, key: string) { + has(_: ComponentRenderContext, key: string) { return key[0] !== '_' && !isGloballyWhitelisted(key) } } @@ -269,7 +255,7 @@ export const RuntimeCompiledPublicInstanceProxyHandlers = { // 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) { +export function createRenderContext(instance: ComponentInternalInstance) { const target: Record = {} // expose internal instance for proxy handlers @@ -302,19 +288,20 @@ export function createDevProxyTarget(instance: ComponentInternalInstance) { }) }) - return target as ComponentPublicProxyTarget + return target as ComponentRenderContext } -export function exposePropsOnDevProxyTarget( +// dev only +export function exposePropsOnRenderContext( instance: ComponentInternalInstance ) { const { - proxyTarget, + ctx, type: { props: propsOptions } } = instance if (propsOptions) { Object.keys(normalizePropsOptions(propsOptions)[0]!).forEach(key => { - Object.defineProperty(proxyTarget, key, { + Object.defineProperty(ctx, key, { enumerable: true, configurable: true, get: () => instance.props[key], @@ -324,12 +311,13 @@ export function exposePropsOnDevProxyTarget( } } -export function exposeSetupStateOnDevProxyTarget( +// dev only +export function exposeSetupStateOnRenderContext( instance: ComponentInternalInstance ) { - const { proxyTarget, setupState } = instance + const { ctx, setupState } = instance Object.keys(toRaw(setupState)).forEach(key => { - Object.defineProperty(proxyTarget, key, { + Object.defineProperty(ctx, key, { enumerable: true, configurable: true, get: () => setupState[key], diff --git a/packages/runtime-core/src/components/KeepAlive.ts b/packages/runtime-core/src/components/KeepAlive.ts index 50a4f756..865cf8d2 100644 --- a/packages/runtime-core/src/components/KeepAlive.ts +++ b/packages/runtime-core/src/components/KeepAlive.ts @@ -26,7 +26,7 @@ import { RendererNode } from '../renderer' import { setTransitionHooks } from './BaseTransition' -import { ComponentPublicProxyTarget } from '../componentProxy' +import { ComponentRenderContext } from '../componentProxy' type MatchPattern = string | RegExp | string[] | RegExp[] @@ -40,7 +40,7 @@ type CacheKey = string | number | Component type Cache = Map type Keys = Set -export interface KeepAliveContext extends ComponentPublicProxyTarget { +export interface KeepAliveContext extends ComponentRenderContext { renderer: RendererInternals activate: ( vnode: VNode, @@ -77,12 +77,12 @@ const KeepAliveImpl = { const instance = getCurrentInstance()! const parentSuspense = instance.suspense - // KeepAlive communicates with the instantiated renderer via the proxyTarget - // as a shared context where the renderer passes in its internals, + // KeepAlive communicates with the instantiated renderer via the + // ctx where the renderer passes in its internals, // and the KeepAlive instance exposes activate/deactivate implementations. // The whole point of this is to avoid importing KeepAlive directly in the // renderer to facilitate tree-shaking. - const sharedContext = instance.proxyTarget as KeepAliveContext + const sharedContext = instance.ctx as KeepAliveContext const { renderer: { p: patch, diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index ad755100..d143ab88 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -949,7 +949,7 @@ function baseCreateRenderer( ) => { if (n1 == null) { if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) { - ;(parentComponent!.proxyTarget as KeepAliveContext).activate( + ;(parentComponent!.ctx as KeepAliveContext).activate( n2, container, anchor, @@ -998,7 +998,7 @@ function baseCreateRenderer( // inject renderer internals for keepAlive if (isKeepAlive(initialVNode)) { - ;(instance.proxyTarget as KeepAliveContext).renderer = internals + ;(instance.ctx as KeepAliveContext).renderer = internals } // resolve props and slots for setup context @@ -1719,7 +1719,7 @@ function baseCreateRenderer( if (shapeFlag & ShapeFlags.COMPONENT) { if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) { - ;(parentComponent!.proxyTarget as KeepAliveContext).deactivate(vnode) + ;(parentComponent!.ctx as KeepAliveContext).deactivate(vnode) } else { unmountComponent(vnode.component!, parentSuspense, doRemove) }