From c97d83aff2d90a6b47c7b29dfe5e28a6954380fe Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 10 Dec 2019 11:14:29 -0500 Subject: [PATCH] refactor(runtime-core): tweak component proxy implementation --- .../runtime-core/__tests__/apiApp.spec.ts | 2 +- .../__tests__/componentProxy.spec.ts | 52 ++++++++++-- .../runtime-core/__tests__/directives.spec.ts | 4 +- packages/runtime-core/src/apiApp.ts | 2 +- packages/runtime-core/src/apiOptions.ts | 2 +- packages/runtime-core/src/apiWatch.ts | 2 +- packages/runtime-core/src/component.ts | 34 ++++++-- packages/runtime-core/src/componentProxy.ts | 82 ++++++++++--------- .../runtime-core/src/componentRenderUtils.ts | 5 +- packages/runtime-core/src/directives.ts | 2 +- packages/runtime-core/src/errorHandling.ts | 2 +- packages/runtime-core/src/renderer.ts | 2 +- packages/runtime-core/src/warning.ts | 2 +- packages/vue/src/index.ts | 4 +- 14 files changed, 133 insertions(+), 64 deletions(-) diff --git a/packages/runtime-core/__tests__/apiApp.spec.ts b/packages/runtime-core/__tests__/apiApp.spec.ts index be76223d..acc25606 100644 --- a/packages/runtime-core/__tests__/apiApp.spec.ts +++ b/packages/runtime-core/__tests__/apiApp.spec.ts @@ -310,7 +310,7 @@ describe('api: createApp', () => { const handler = (app.config.warnHandler = jest.fn( (msg, instance, trace) => { expect(msg).toMatch(`Component is missing template or render function`) - expect(instance).toBe(ctx.renderProxy) + expect(instance).toBe(ctx.proxy) expect(trace).toMatch(`Hello`) } )) diff --git a/packages/runtime-core/__tests__/componentProxy.spec.ts b/packages/runtime-core/__tests__/componentProxy.spec.ts index 4c46875c..2014cd2f 100644 --- a/packages/runtime-core/__tests__/componentProxy.spec.ts +++ b/packages/runtime-core/__tests__/componentProxy.spec.ts @@ -9,7 +9,7 @@ import { ComponentInternalInstance } from '../src/component' describe('component: proxy', () => { mockWarn() - it('data', () => { + test('data', () => { const app = createApp() let instance: ComponentInternalInstance let instanceProxy: any @@ -33,7 +33,7 @@ describe('component: proxy', () => { expect(instance!.data.foo).toBe(2) }) - it('renderContext', () => { + test('renderContext', () => { const app = createApp() let instance: ComponentInternalInstance let instanceProxy: any @@ -57,7 +57,7 @@ describe('component: proxy', () => { expect(instance!.renderContext.foo).toBe(2) }) - it('propsProxy', () => { + test('propsProxy', () => { const app = createApp() let instance: ComponentInternalInstance let instanceProxy: any @@ -83,7 +83,7 @@ describe('component: proxy', () => { expect(`Attempting to mutate prop "foo"`).toHaveBeenWarned() }) - it('methods', () => { + test('public properties', () => { const app = createApp() let instance: ComponentInternalInstance let instanceProxy: any @@ -111,7 +111,7 @@ describe('component: proxy', () => { expect(`Attempting to mutate public property "$data"`).toHaveBeenWarned() }) - it('sink', async () => { + test('sink', async () => { const app = createApp() let instance: ComponentInternalInstance let instanceProxy: any @@ -129,4 +129,46 @@ describe('component: proxy', () => { expect(instanceProxy.foo).toBe(1) expect(instance!.sink.foo).toBe(1) }) + + test('has check', () => { + const app = createApp() + let instanceProxy: any + const Comp = { + render() {}, + props: { + msg: String + }, + data() { + return { + foo: 0 + } + }, + setup() { + return { + bar: 1 + } + }, + mounted() { + instanceProxy = this + } + } + app.mount(Comp, nodeOps.createElement('div'), { msg: 'hello' }) + + // props + expect('msg' in instanceProxy).toBe(true) + // data + expect('foo' in instanceProxy).toBe(true) + // renderContext + expect('bar' in instanceProxy).toBe(true) + // public properties + expect('$el' in instanceProxy).toBe(true) + + // non-existent + expect('$foobar' in instanceProxy).toBe(false) + expect('baz' in instanceProxy).toBe(false) + + // set non-existent (goes into sink) + instanceProxy.baz = 1 + expect('baz' in instanceProxy).toBe(true) + }) }) diff --git a/packages/runtime-core/__tests__/directives.spec.ts b/packages/runtime-core/__tests__/directives.spec.ts index 50baa150..e8e64bb7 100644 --- a/packages/runtime-core/__tests__/directives.spec.ts +++ b/packages/runtime-core/__tests__/directives.spec.ts @@ -18,7 +18,7 @@ describe('directives', () => { function assertBindings(binding: DirectiveBinding) { expect(binding.value).toBe(count.value) expect(binding.arg).toBe('foo') - expect(binding.instance).toBe(_instance && _instance.renderProxy) + expect(binding.instance).toBe(_instance && _instance.proxy) expect(binding.modifiers && binding.modifiers.ok).toBe(true) } @@ -151,7 +151,7 @@ describe('directives', () => { function assertBindings(binding: DirectiveBinding) { expect(binding.value).toBe(count.value) expect(binding.arg).toBe('foo') - expect(binding.instance).toBe(_instance && _instance.renderProxy) + expect(binding.instance).toBe(_instance && _instance.proxy) expect(binding.modifiers && binding.modifiers.ok).toBe(true) } diff --git a/packages/runtime-core/src/apiApp.ts b/packages/runtime-core/src/apiApp.ts index 53614ca2..defe9af8 100644 --- a/packages/runtime-core/src/apiApp.ts +++ b/packages/runtime-core/src/apiApp.ts @@ -177,7 +177,7 @@ export function createAppAPI( vnode.appContext = context render(vnode, rootContainer) isMounted = true - return vnode.component!.renderProxy + return vnode.component!.proxy } else if (__DEV__) { warn( `App has already been mounted. Create a new app instance instead.` diff --git a/packages/runtime-core/src/apiOptions.ts b/packages/runtime-core/src/apiOptions.ts index 91fc9465..a3aad5df 100644 --- a/packages/runtime-core/src/apiOptions.ts +++ b/packages/runtime-core/src/apiOptions.ts @@ -215,7 +215,7 @@ export function applyOptions( instance.renderContext === EMPTY_OBJ ? (instance.renderContext = reactive({})) : instance.renderContext - const ctx = instance.renderProxy! + const ctx = instance.proxy! const { // composition mixins, diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index e6401fcd..efd3dfd4 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -220,7 +220,7 @@ export function instanceWatch( cb: Function, options?: WatchOptions ): StopHandle { - const ctx = this.renderProxy as Data + const ctx = this.proxy as Data const getter = isString(source) ? () => ctx[source] : source.bind(ctx) const stop = watch(getter, cb.bind(ctx), options) onBeforeUnmount(stop, this) diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 45c05038..8fbd81e8 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -2,7 +2,8 @@ import { VNode, VNodeChild, isVNode } from './vnode' import { ReactiveEffect, reactive, shallowReadonly } from '@vue/reactivity' import { PublicInstanceProxyHandlers, - ComponentPublicInstance + ComponentPublicInstance, + runtimeCompiledRenderProxyHandlers } from './componentProxy' import { ComponentPropsOptions } from './componentProps' import { Slots } from './componentSlots' @@ -68,7 +69,10 @@ export interface SetupContext { emit: Emit } -export type RenderFunction = () => VNodeChild +export type RenderFunction = { + (): VNodeChild + isRuntimeCompiled?: boolean +} export interface ComponentInternalInstance { type: FunctionalComponent | ComponentOptions @@ -82,7 +86,7 @@ export interface ComponentInternalInstance { render: RenderFunction | null effects: ReactiveEffect[] | null provides: Data - // cache for renderProxy access type to avoid hasOwnProperty calls + // cache for proxy access type to avoid hasOwnProperty calls accessCache: Data | null // cache for render function values that rely on _ctx but won't need updates // after initialized (e.g. inline handlers) @@ -98,7 +102,10 @@ export interface ComponentInternalInstance { props: Data attrs: Data slots: Slots - renderProxy: ComponentPublicInstance | null + proxy: ComponentPublicInstance | null + // alternative proxy used only for runtime-compiled render functions using + // `with` block + withProxy: ComponentPublicInstance | null propsProxy: Data | null setupContext: SetupContext | null refs: Data @@ -150,7 +157,8 @@ export function createComponentInstance( subTree: null!, // will be set synchronously right after creation update: null!, // will be set synchronously right after creation render: null, - renderProxy: null, + proxy: null, + withProxy: null, propsProxy: null, setupContext: null, effects: null, @@ -264,8 +272,8 @@ export function setupStatefulComponent( } // 0. create render proxy property access cache instance.accessCache = {} - // 1. create render proxy - instance.renderProxy = new Proxy(instance, PublicInstanceProxyHandlers) + // 1. create public instance / render proxy + instance.proxy = new Proxy(instance, PublicInstanceProxyHandlers) // 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 @@ -371,6 +379,7 @@ function finishComponentSetup( } }) } + if (__DEV__ && !Component.render) { /* istanbul ignore if */ if (!__RUNTIME_COMPILE__ && Component.template) { @@ -387,7 +396,18 @@ function finishComponentSetup( ) } } + instance.render = (Component.render || NOOP) as RenderFunction + + // for runtime-compiled render functions using `with` blocks, the render + // proxy used needs a different `has` handler which is more performant and + // also only allows a whitelist of globals to fallthrough. + if (__RUNTIME_COMPILE__ && instance.render.isRuntimeCompiled) { + instance.withProxy = new Proxy( + instance, + runtimeCompiledRenderProxyHandlers + ) + } } // support for 2.x options diff --git a/packages/runtime-core/src/componentProxy.ts b/packages/runtime-core/src/componentProxy.ts index 53f5aeba..0638b9bd 100644 --- a/packages/runtime-core/src/componentProxy.ts +++ b/packages/runtime-core/src/componentProxy.ts @@ -45,16 +45,25 @@ export type ComponentPublicInstance< ExtractComputedReturns & M -const publicPropertiesMap = { - $data: 'data', - $props: 'propsProxy', - $attrs: 'attrs', - $slots: 'slots', - $refs: 'refs', - $parent: 'parent', - $root: 'root', - $emit: 'emit', - $options: 'type' +const publicPropertiesMap: Record< + string, + (i: ComponentInternalInstance) => any +> = { + $: i => i, + $el: i => i.vnode.el, + $cache: i => i.renderCache, + $data: i => i.data, + $props: i => i.propsProxy, + $attrs: i => i.attrs, + $slots: i => i.slots, + $refs: i => i.refs, + $parent: i => i.parent, + $root: i => i.root, + $emit: i => i.emit, + $options: i => i.type, + $forceUpdate: i => i.update, + $nextTick: () => nextTick, + $watch: i => instanceWatch.bind(i) } const enum AccessTypes { @@ -78,6 +87,8 @@ export const PublicInstanceProxyHandlers: ProxyHandler = { type, sink } = target + + // data / props / renderContext // 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 @@ -106,31 +117,16 @@ export const PublicInstanceProxyHandlers: ProxyHandler = { } // return the value from propsProxy for ref unwrapping and readonly return propsProxy![key] - } else if (key === '$') { - // reserved backdoor to access the internal instance - return target - } else if (key === '$cache') { - return target.renderCache || (target.renderCache = []) - } else if (key === '$el') { - return target.vnode.el - } else if (hasOwn(publicPropertiesMap, key)) { + } + + // public $xxx properties & user-attached properties (sink) + const publicGetter = publicPropertiesMap[key] + if (publicGetter !== undefined) { if (__DEV__ && key === '$attrs') { markAttrsAccessed() } - return target[publicPropertiesMap[key]] - } - // methods are only exposed when options are supported - if (__FEATURE_OPTIONS__) { - switch (key) { - case '$forceUpdate': - return target.update - case '$nextTick': - return nextTick - case '$watch': - return instanceWatch.bind(target) - } - } - if (hasOwn(sink, key)) { + return publicGetter(target) + } else if (hasOwn(sink, key)) { return sink[key] } else if (__DEV__ && currentRenderingInstance != null) { warn( @@ -140,6 +136,18 @@ export const PublicInstanceProxyHandlers: ProxyHandler = { } }, + has(target: ComponentInternalInstance, key: string) { + const { data, accessCache, renderContext, type, sink } = target + return ( + accessCache![key] !== undefined || + (data !== EMPTY_OBJ && hasOwn(data, key)) || + hasOwn(renderContext, key) || + (type.props != null && hasOwn(type.props, key)) || + hasOwn(publicPropertiesMap, key) || + hasOwn(sink, key) + ) + }, + set(target: ComponentInternalInstance, key: string, value: any): boolean { const { data, renderContext } = target if (data !== EMPTY_OBJ && hasOwn(data, key)) { @@ -165,13 +173,9 @@ export const PublicInstanceProxyHandlers: ProxyHandler = { } } -if (__RUNTIME_COMPILE__) { - // this trap is only called in browser-compiled render functions that use - // `with (this) {}` - PublicInstanceProxyHandlers.has = ( - _: ComponentInternalInstance, - key: string - ): boolean => { +export const runtimeCompiledRenderProxyHandlers = { + ...PublicInstanceProxyHandlers, + has(_target: ComponentInternalInstance, key: string) { return key[0] !== '_' && !isGloballyWhitelisted(key) } } diff --git a/packages/runtime-core/src/componentRenderUtils.ts b/packages/runtime-core/src/componentRenderUtils.ts index db8bd527..4d5799ca 100644 --- a/packages/runtime-core/src/componentRenderUtils.ts +++ b/packages/runtime-core/src/componentRenderUtils.ts @@ -34,7 +34,8 @@ export function renderComponentRoot( const { type: Component, vnode, - renderProxy, + proxy, + withProxy, props, slots, attrs, @@ -48,7 +49,7 @@ export function renderComponentRoot( } try { if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { - result = normalizeVNode(instance.render!.call(renderProxy)) + result = normalizeVNode(instance.render!.call(withProxy || proxy)) } else { // functional const render = Component as FunctionalComponent diff --git a/packages/runtime-core/src/directives.ts b/packages/runtime-core/src/directives.ts index 4f80bb1f..132fbbe8 100644 --- a/packages/runtime-core/src/directives.ts +++ b/packages/runtime-core/src/directives.ts @@ -113,7 +113,7 @@ export function withDirectives( __DEV__ && warn(`withDirectives can only be used inside render functions.`) return vnode } - const instance = internalInstance.renderProxy + const instance = internalInstance.proxy const props = vnode.props || (vnode.props = {}) const bindings = vnode.dirs || (vnode.dirs = new Array(directives.length)) const injected: Record = {} diff --git a/packages/runtime-core/src/errorHandling.ts b/packages/runtime-core/src/errorHandling.ts index 2551fac5..29137a4e 100644 --- a/packages/runtime-core/src/errorHandling.ts +++ b/packages/runtime-core/src/errorHandling.ts @@ -99,7 +99,7 @@ export function handleError( if (instance) { let cur = instance.parent // the exposed instance is the render proxy to keep it consistent with 2.x - const exposedInstance = instance.renderProxy + const exposedInstance = instance.proxy // in production the hook receives only the error code const errorInfo = __DEV__ ? ErrorTypeStrings[type] : type while (cur) { diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 91de84c7..2c3d65f3 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -840,7 +840,7 @@ export function createRenderer< ) popWarningContext() } - setRef(n2.ref, n1 && n1.ref, parentComponent, n2.component!.renderProxy) + setRef(n2.ref, n1 && n1.ref, parentComponent, n2.component!.proxy) } } diff --git a/packages/runtime-core/src/warning.ts b/packages/runtime-core/src/warning.ts index a79c5d3f..083bda55 100644 --- a/packages/runtime-core/src/warning.ts +++ b/packages/runtime-core/src/warning.ts @@ -41,7 +41,7 @@ export function warn(msg: string, ...args: any[]) { ErrorCodes.APP_WARN_HANDLER, [ msg + args.join(''), - instance && instance.renderProxy, + instance && instance.proxy, trace .map(({ vnode }) => `at <${formatComponentName(vnode)}>`) .join('\n'), diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts index fcddd2a4..1bdf58c2 100644 --- a/packages/vue/src/index.ts +++ b/packages/vue/src/index.ts @@ -36,7 +36,9 @@ function compileToFunction( ...options }) - return new Function('Vue', code)(runtimeDom) as RenderFunction + const render = new Function('Vue', code)(runtimeDom) as RenderFunction + render.isRuntimeCompiled = true + return render } registerRuntimeCompiler(compileToFunction)