]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
refactor(runtime-core): tweak component proxy implementation
authorEvan You <yyx990803@gmail.com>
Tue, 10 Dec 2019 16:14:29 +0000 (11:14 -0500)
committerEvan You <yyx990803@gmail.com>
Tue, 10 Dec 2019 16:14:29 +0000 (11:14 -0500)
14 files changed:
packages/runtime-core/__tests__/apiApp.spec.ts
packages/runtime-core/__tests__/componentProxy.spec.ts
packages/runtime-core/__tests__/directives.spec.ts
packages/runtime-core/src/apiApp.ts
packages/runtime-core/src/apiOptions.ts
packages/runtime-core/src/apiWatch.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/componentProxy.ts
packages/runtime-core/src/componentRenderUtils.ts
packages/runtime-core/src/directives.ts
packages/runtime-core/src/errorHandling.ts
packages/runtime-core/src/renderer.ts
packages/runtime-core/src/warning.ts
packages/vue/src/index.ts

index be76223dc35575596c22c7ccdd557d88edac5543..acc25606d7f50abd59dfca832ff229f437a8122d 100644 (file)
@@ -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`)
       }
     ))
index 4c46875c0bdf44c12f9ab4ab1ec664e5e430af59..2014cd2f8e50489d6b5209adea25a6b21d510a44 100644 (file)
@@ -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)
+  })
 })
index 50baa150be536a277a91abeb257bdf7b861fb0e8..e8e64bb78ec6196fe2b0aa3f6b173bb2b6193bdb 100644 (file)
@@ -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)
     }
 
index 53614ca2eea118190dec114eff4f027f41afaa9b..defe9af8cd33b5b25cf955764616d9b6b51e9cff 100644 (file)
@@ -177,7 +177,7 @@ export function createAppAPI<HostNode, HostElement>(
           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.`
index 91fc946587dc614bbfc19466a9c60e357f7a2f12..a3aad5df365ac9884d39ba67034c2f37e6286ac6 100644 (file)
@@ -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,
index e6401fcdc8ab0d19bb1defa36de8f56e3b3d0642..efd3dfd40a4f9ee4272be88257626060e453c405 100644 (file)
@@ -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)
index 45c05038e6b47dc361a6c2870c1c1e38d25b6bdf..8fbd81e82adc435397eb7dd5d169e0cfe54e4222 100644 (file)
@@ -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
index 53f5aeba47084795d6381a967e58380ec573f285..0638b9bd3c669bda6fa709cff9f93b8c7a278e5f 100644 (file)
@@ -45,16 +45,25 @@ export type ComponentPublicInstance<
   ExtractComputedReturns<C> &
   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<any> = {
       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<any> = {
       }
       // 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<any> = {
     }
   },
 
+  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<any> = {
   }
 }
 
-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)
   }
 }
index db8bd527eb4b99106ea46dda35aed5a326f7576f..4d5799ca0c4c43cca8a5e7bfcdef576c13b374df 100644 (file)
@@ -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
index 4f80bb1f23171261199e624e394fc6f2a544073b..132fbbe842ac73eec5e1933775cf9daf22e95eda 100644 (file)
@@ -113,7 +113,7 @@ export function withDirectives<T extends VNode>(
     __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<string, true> = {}
index 2551fac571f9e95a612c1cfe9892d1461dd0b020..29137a4e2650392971c34be89dd770d84d8a53ba 100644 (file)
@@ -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) {
index 91de84c72630b3b66d2e577e30d6f0987ed05f1e..2c3d65f361368a427518005dfcfae140d56a4240 100644 (file)
@@ -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)
     }
   }
 
index a79c5d3fe6b365d508234da8fcb9e7ace0c9519e..083bda553e03ff011589d31544ee623e580d16d0 100644 (file)
@@ -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'),
index fcddd2a4e5b3e31515a8fb378af90ee5363b43ec..1bdf58c2d07f38f6ae3afe1b7b8f4c9c6dc4a908 100644 (file)
@@ -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)