]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(runtime-core): improve component public instance proxy inspection
authorEvan You <yyx990803@gmail.com>
Sun, 5 Apr 2020 22:39:22 +0000 (18:39 -0400)
committerEvan You <yyx990803@gmail.com>
Sun, 5 Apr 2020 22:39:22 +0000 (18:39 -0400)
packages/runtime-core/__tests__/componentProxy.spec.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/componentOptions.ts
packages/runtime-core/src/componentProxy.ts
packages/runtime-core/src/components/Suspense.ts

index ee8f2bc75d57e6fd4f6e1884cc7b59ba624a89f9..a6e2181abb6f20340c27a978f4fc5827bdd22c50 100644 (file)
@@ -108,8 +108,10 @@ describe('component: proxy', () => {
     expect(instanceProxy.$attrs).toBe(instance!.attrs)
     expect(instanceProxy.$slots).toBe(instance!.slots)
     expect(instanceProxy.$refs).toBe(instance!.refs)
-    expect(instanceProxy.$parent).toBe(instance!.parent)
-    expect(instanceProxy.$root).toBe(instance!.root)
+    expect(instanceProxy.$parent).toBe(
+      instance!.parent && instance!.parent.proxy
+    )
+    expect(instanceProxy.$root).toBe(instance!.root.proxy)
     expect(instanceProxy.$emit).toBe(instance!.emit)
     expect(instanceProxy.$el).toBe(instance!.vnode.el)
     expect(instanceProxy.$options).toBe(instance!.type)
@@ -174,6 +176,14 @@ describe('component: proxy', () => {
     // set non-existent (goes into sink)
     instanceProxy.baz = 1
     expect('baz' in instanceProxy).toBe(true)
+
+    // dev mode ownKeys check for console inspection
+    expect(Object.keys(instanceProxy)).toMatchObject([
+      'msg',
+      'bar',
+      'foo',
+      'baz'
+    ])
   })
 
   // #864
index 6b7fdb9c366a6c33b217336a6b3a66a4c87669df..43e5d547690410ebb246ec66229abd7af53827b4 100644 (file)
@@ -7,9 +7,13 @@ import {
   resetTracking
 } from '@vue/reactivity'
 import {
-  PublicInstanceProxyHandlers,
   ComponentPublicInstance,
-  runtimeCompiledRenderProxyHandlers
+  ComponentPublicProxyTarget,
+  PublicInstanceProxyHandlers,
+  RuntimeCompiledPublicInstanceProxyHandlers,
+  createDevProxyTarget,
+  exposePropsOnDevProxyTarget,
+  exposeRenderContextOnDevProxyTarget
 } from './componentProxy'
 import { ComponentPropsOptions, resolveProps } from './componentProps'
 import { Slots, resolveSlots } from './componentSlots'
@@ -139,6 +143,7 @@ export interface ComponentInternalInstance {
   attrs: Data
   slots: Slots
   proxy: ComponentPublicInstance | null
+  proxyTarget: ComponentPublicProxyTarget
   // alternative proxy used only for runtime-compiled render functions using
   // `with` block
   withProxy: ComponentPublicInstance | null
@@ -195,12 +200,13 @@ export function createComponentInstance(
     parent,
     appContext,
     type: vnode.type as Component,
-    root: null!, // set later so it can point to itself
+    root: null!, // to be immediately set
     next: null,
     subTree: null!, // will be set synchronously right after creation
     update: null!, // will be set synchronously right after creation
     render: null,
     proxy: null,
+    proxyTarget: null!, // to be immediately set
     withProxy: null,
     propsProxy: null,
     setupContext: null,
@@ -250,6 +256,11 @@ export function createComponentInstance(
     ec: null,
     emit: null as any // to be set immediately
   }
+  if (__DEV__) {
+    instance.proxyTarget = createDevProxyTarget(instance)
+  } else {
+    instance.proxyTarget = { _: instance }
+  }
   instance.root = parent ? parent.root : instance
   instance.emit = emit.bind(null, instance)
   return instance
@@ -325,7 +336,10 @@ function setupStatefulComponent(
   // 0. create render proxy property access cache
   instance.accessCache = {}
   // 1. create public instance / render proxy
-  instance.proxy = new Proxy(instance, PublicInstanceProxyHandlers)
+  instance.proxy = new Proxy(instance.proxyTarget, PublicInstanceProxyHandlers)
+  if (__DEV__) {
+    exposePropsOnDevProxyTarget(instance)
+  }
   // 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
@@ -353,7 +367,7 @@ function setupStatefulComponent(
       if (isSSR) {
         // return the promise so server-renderer can wait on it
         return setupResult.then((resolvedResult: unknown) => {
-          handleSetupResult(instance, resolvedResult, parentSuspense, isSSR)
+          handleSetupResult(instance, resolvedResult, isSSR)
         })
       } else if (__FEATURE_SUSPENSE__) {
         // async setup returned Promise.
@@ -366,7 +380,7 @@ function setupStatefulComponent(
         )
       }
     } else {
-      handleSetupResult(instance, setupResult, parentSuspense, isSSR)
+      handleSetupResult(instance, setupResult, isSSR)
     }
   } else {
     finishComponentSetup(instance, isSSR)
@@ -376,7 +390,6 @@ function setupStatefulComponent(
 export function handleSetupResult(
   instance: ComponentInternalInstance,
   setupResult: unknown,
-  parentSuspense: SuspenseBoundary | null,
   isSSR: boolean
 ) {
   if (isFunction(setupResult)) {
@@ -392,6 +405,9 @@ export function handleSetupResult(
     // setup returned bindings.
     // assuming a render function compiled from template is present.
     instance.renderContext = reactive(setupResult)
+    if (__DEV__) {
+      exposeRenderContextOnDevProxyTarget(instance)
+    }
   } else if (__DEV__ && setupResult !== undefined) {
     warn(
       `setup() should return an object. Received: ${
@@ -460,8 +476,8 @@ function finishComponentSetup(
     // also only allows a whitelist of globals to fallthrough.
     if (instance.render._rc) {
       instance.withProxy = new Proxy(
-        instance,
-        runtimeCompiledRenderProxyHandlers
+        instance.proxyTarget,
+        RuntimeCompiledPublicInstanceProxyHandlers
       )
     }
   }
index 842e9a0cebc6034f06c1b7d55feeefdc146a1d24..ffd086983df4fe6d6858bfa4bbaa307dd7dbfabc 100644 (file)
@@ -38,9 +38,14 @@ import {
 import {
   reactive,
   ComputedGetter,
-  WritableComputedOptions
+  WritableComputedOptions,
+  ComputedRef
 } from '@vue/reactivity'
-import { ComponentObjectPropsOptions, ExtractPropTypes } from './componentProps'
+import {
+  ComponentObjectPropsOptions,
+  ExtractPropTypes,
+  normalizePropsOptions
+} from './componentProps'
 import { EmitsOptions } from './componentEmits'
 import { Directive } from './directives'
 import { ComponentPublicInstance } from './componentProxy'
@@ -239,6 +244,7 @@ export function applyOptions(
   options: ComponentOptions,
   asMixin: boolean = false
 ) {
+  const proxyTarget = instance.proxyTarget
   const ctx = instance.proxy!
   const {
     // composition
@@ -277,7 +283,7 @@ export function applyOptions(
 
   const globalMixins = instance.appContext.mixins
   // call it only during dev
-  const checkDuplicateProperties = __DEV__ ? createDuplicateChecker() : null
+
   // applyOptions is called non-as-mixin once per instance
   if (!asMixin) {
     callSyncHook('beforeCreate', options, ctx, globalMixins)
@@ -293,8 +299,10 @@ export function applyOptions(
     applyMixins(instance, mixins)
   }
 
+  const checkDuplicateProperties = __DEV__ ? createDuplicateChecker() : null
+
   if (__DEV__ && propsOptions) {
-    for (const key in propsOptions) {
+    for (const key in normalizePropsOptions(propsOptions)[0]) {
       checkDuplicateProperties!(OptionTypes.PROPS, key)
     }
   }
@@ -314,6 +322,7 @@ export function applyOptions(
       if (__DEV__) {
         for (const key in data) {
           checkDuplicateProperties!(OptionTypes.DATA, key)
+          if (!(key in proxyTarget)) proxyTarget[key] = data[key]
         }
       }
       instance.data = reactive(data)
@@ -326,9 +335,6 @@ export function applyOptions(
   if (computedOptions) {
     for (const key in computedOptions) {
       const opt = (computedOptions as ComputedOptions)[key]
-
-      __DEV__ && checkDuplicateProperties!(OptionTypes.COMPUTED, key)
-
       if (isFunction(opt)) {
         renderContext[key] = computed(opt.bind(ctx, ctx))
       } else {
@@ -350,6 +356,15 @@ export function applyOptions(
           warn(`Computed property "${key}" has no getter.`)
         }
       }
+      if (__DEV__) {
+        checkDuplicateProperties!(OptionTypes.COMPUTED, key)
+        if (renderContext[key] && !(key in proxyTarget)) {
+          Object.defineProperty(proxyTarget, key, {
+            enumerable: true,
+            get: () => (renderContext[key] as ComputedRef).value
+          })
+        }
+      }
     }
   }
 
@@ -357,8 +372,13 @@ export function applyOptions(
     for (const key in methods) {
       const methodHandler = (methods as MethodOptions)[key]
       if (isFunction(methodHandler)) {
-        __DEV__ && checkDuplicateProperties!(OptionTypes.METHODS, key)
         renderContext[key] = methodHandler.bind(ctx)
+        if (__DEV__) {
+          checkDuplicateProperties!(OptionTypes.METHODS, key)
+          if (!(key in proxyTarget)) {
+            proxyTarget[key] = renderContext[key]
+          }
+        }
       } else if (__DEV__) {
         warn(
           `Method "${key}" has type "${typeof methodHandler}" in the component definition. ` +
@@ -387,18 +407,24 @@ export function applyOptions(
     if (isArray(injectOptions)) {
       for (let i = 0; i < injectOptions.length; i++) {
         const key = injectOptions[i]
-        __DEV__ && checkDuplicateProperties!(OptionTypes.INJECT, key)
         renderContext[key] = inject(key)
+        if (__DEV__) {
+          checkDuplicateProperties!(OptionTypes.INJECT, key)
+          proxyTarget[key] = renderContext[key]
+        }
       }
     } else {
       for (const key in injectOptions) {
-        __DEV__ && checkDuplicateProperties!(OptionTypes.INJECT, key)
         const opt = injectOptions[key]
         if (isObject(opt)) {
           renderContext[key] = inject(opt.from, opt.default)
         } else {
           renderContext[key] = inject(opt)
         }
+        if (__DEV__) {
+          checkDuplicateProperties!(OptionTypes.INJECT, key)
+          proxyTarget[key] = renderContext[key]
+        }
       }
     }
   }
index ef46ffbfe0c9a509997a5cf5ddc87096d7576283..032e42fc480cb441281a91ef8f77aabe6ee14df3 100644 (file)
@@ -2,7 +2,7 @@ import { ComponentInternalInstance, Data } from './component'
 import { nextTick, queueJob } from './scheduler'
 import { instanceWatch } from './apiWatch'
 import { EMPTY_OBJ, hasOwn, isGloballyWhitelisted, NOOP } from '@vue/shared'
-import { ReactiveEffect, UnwrapRef } from '@vue/reactivity'
+import { ReactiveEffect, UnwrapRef, toRaw } from '@vue/reactivity'
 import {
   ExtractComputedReturns,
   ComponentOptionsBase,
@@ -61,8 +61,8 @@ const publicPropertiesMap: Record<
   $attrs: i => i.attrs,
   $slots: i => i.slots,
   $refs: i => i.refs,
-  $parent: i => i.parent,
-  $root: i => i.root,
+  $parent: i => i.parent && i.parent.proxy,
+  $root: i => i.root && i.root.proxy,
   $emit: i => i.emit,
   $options: i => (__FEATURE_OPTIONS__ ? resolveMergedOptions(i) : i.type),
   $forceUpdate: i => () => queueJob(i.update),
@@ -77,8 +77,13 @@ const enum AccessTypes {
   OTHER
 }
 
+export interface ComponentPublicProxyTarget {
+  [key: string]: any
+  _: ComponentInternalInstance
+}
+
 export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
-  get(target: ComponentInternalInstance, key: string) {
+  get({ _: instance }: ComponentPublicProxyTarget, key: string) {
     const {
       renderContext,
       data,
@@ -87,7 +92,7 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
       type,
       sink,
       appContext
-    } = target
+    } = instance
 
     // data / props / renderContext
     // This getter gets called for every property access on the render context
@@ -133,7 +138,7 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
       if (__DEV__ && key === '$attrs') {
         markAttrsAccessed()
       }
-      return publicGetter(target)
+      return publicGetter(instance)
     } else if (hasOwn(sink, key)) {
       return sink[key]
     } else if (
@@ -154,53 +159,131 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
     }
   },
 
-  has(target: ComponentInternalInstance, key: string) {
-    const { data, accessCache, renderContext, type, sink } = target
+  has(
+    {
+      _: { data, accessCache, renderContext, type, sink }
+    }: ComponentPublicProxyTarget,
+    key: string
+  ) {
     return (
       accessCache![key] !== undefined ||
       (data !== EMPTY_OBJ && hasOwn(data, key)) ||
       hasOwn(renderContext, key) ||
-      (type.props && hasOwn(type.props, key)) ||
+      (type.props && hasOwn(normalizePropsOptions(type.props)[0], key)) ||
       hasOwn(publicPropertiesMap, key) ||
       hasOwn(sink, key)
     )
   },
 
-  set(target: ComponentInternalInstance, key: string, value: any): boolean {
-    const { data, renderContext } = target
+  set(
+    { _: instance }: ComponentPublicProxyTarget,
+    key: string,
+    value: any
+  ): boolean {
+    const { data, renderContext } = instance
     if (data !== EMPTY_OBJ && hasOwn(data, key)) {
       data[key] = value
     } else if (hasOwn(renderContext, key)) {
       renderContext[key] = value
-    } else if (key[0] === '$' && key.slice(1) in target) {
+    } else if (key[0] === '$' && key.slice(1) in instance) {
       __DEV__ &&
         warn(
           `Attempting to mutate public property "${key}". ` +
             `Properties starting with $ are reserved and readonly.`,
-          target
+          instance
         )
       return false
-    } else if (key in target.props) {
+    } else if (key in instance.props) {
       __DEV__ &&
-        warn(`Attempting to mutate prop "${key}". Props are readonly.`, target)
+        warn(
+          `Attempting to mutate prop "${key}". Props are readonly.`,
+          instance
+        )
       return false
     } else {
-      target.sink[key] = value
+      instance.sink[key] = value
+      if (__DEV__) {
+        instance.proxyTarget[key] = value
+      }
     }
     return true
   }
 }
 
-export const runtimeCompiledRenderProxyHandlers = {
+export const RuntimeCompiledPublicInstanceProxyHandlers = {
   ...PublicInstanceProxyHandlers,
-  get(target: ComponentInternalInstance, key: string) {
+  get(target: ComponentPublicProxyTarget, key: string) {
     // fast path for unscopables when using `with` block
     if ((key as any) === Symbol.unscopables) {
       return
     }
     return PublicInstanceProxyHandlers.get!(target, key, target)
   },
-  has(_target: ComponentInternalInstance, key: string) {
+  has(_: ComponentPublicProxyTarget, key: string) {
     return key[0] !== '_' && !isGloballyWhitelisted(key)
   }
 }
+
+// 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) {
+  const target: Record<string, any> = {}
+
+  // expose internal instance for proxy handlers
+  Object.defineProperty(target, `_`, {
+    get: () => instance
+  })
+
+  // expose public properties
+  Object.keys(publicPropertiesMap).forEach(key => {
+    Object.defineProperty(target, key, {
+      get: () => publicPropertiesMap[key](instance)
+    })
+  })
+
+  // expose global properties
+  const { globalProperties } = instance.appContext.config
+  Object.keys(globalProperties).forEach(key => {
+    Object.defineProperty(target, key, {
+      get: () => globalProperties[key]
+    })
+  })
+
+  return target as ComponentPublicProxyTarget
+}
+
+export function exposePropsOnDevProxyTarget(
+  instance: ComponentInternalInstance
+) {
+  const {
+    proxyTarget,
+    type: { props: propsOptions }
+  } = instance
+  if (propsOptions) {
+    Object.keys(normalizePropsOptions(propsOptions)[0]).forEach(key => {
+      Object.defineProperty(proxyTarget, key, {
+        enumerable: true,
+        get: () => instance.props[key],
+        // intercepted by the proxy so no need for implementation,
+        // but needed to prevent set errors
+        set: NOOP
+      })
+    })
+  }
+}
+
+export function exposeRenderContextOnDevProxyTarget(
+  instance: ComponentInternalInstance
+) {
+  const { proxyTarget, renderContext } = instance
+  Object.keys(toRaw(renderContext)).forEach(key => {
+    Object.defineProperty(proxyTarget, key, {
+      enumerable: true,
+      get: () => renderContext[key],
+      // intercepted by the proxy so no need for implementation,
+      // but needed to prevent set errors
+      set: NOOP
+    })
+  })
+}
index 4e925830ca2942fe1d701041f6bc9bb68cf5a9d1..f5934fa0b70571d2580bbb880bb84dbde466857f 100644 (file)
@@ -419,7 +419,7 @@ function createSuspenseBoundary(
           if (__DEV__) {
             pushWarningContext(vnode)
           }
-          handleSetupResult(instance, asyncSetupResult, suspense, false)
+          handleSetupResult(instance, asyncSetupResult, false)
           if (hydratedEl) {
             // vnode may have been replaced if an update happened before the
             // async dep is reoslved.