Просмотр исходного кода

refactor(runtime-core): tweak component proxy implementation

Evan You 6 лет назад
Родитель
Сommit
c97d83aff2

+ 1 - 1
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`)
       }
     ))

+ 47 - 5
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)
+  })
 })

+ 2 - 2
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)
     }
 

+ 1 - 1
packages/runtime-core/src/apiApp.ts

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

+ 1 - 1
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,

+ 1 - 1
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)

+ 27 - 7
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

+ 43 - 39
packages/runtime-core/src/componentProxy.ts

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

+ 3 - 2
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

+ 1 - 1
packages/runtime-core/src/directives.ts

@@ -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> = {}

+ 1 - 1
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) {

+ 1 - 1
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)
     }
   }
 

+ 1 - 1
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'),

+ 3 - 1
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)