]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(expose): always expose $ instance properties on child refs
authorEvan You <yyx990803@gmail.com>
Fri, 25 Jun 2021 01:28:09 +0000 (21:28 -0400)
committerEvan You <yyx990803@gmail.com>
Fri, 25 Jun 2021 01:28:09 +0000 (21:28 -0400)
packages/runtime-core/__tests__/apiExpose.spec.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/componentOptions.ts
packages/runtime-core/src/componentPublicInstance.ts
packages/runtime-core/src/renderer.ts

index a74edfad543308cacf2869c1cbe90498e959f186..fb957b68db3d0ed8dc26945ad638af7035e1117e 100644 (file)
@@ -7,7 +7,7 @@ describe('api: expose', () => {
       render() {},
       setup(_, { expose }) {
         expose({
-          foo: ref(1),
+          foo: 1,
           bar: ref(2)
         })
         return {
@@ -169,4 +169,26 @@ describe('api: expose', () => {
     const root = nodeOps.createElement('div')
     render(h(Parent), root)
   })
+
+  test('expose should allow access to built-in instance properties', () => {
+    const Child = defineComponent({
+      render() {
+        return h('div')
+      },
+      setup(_, { expose }) {
+        expose()
+        return {}
+      }
+    })
+
+    const childRef = ref()
+    const Parent = {
+      setup() {
+        return () => h(Child, { ref: childRef })
+      }
+    }
+    const root = nodeOps.createElement('div')
+    render(h(Parent), root)
+    expect(childRef.value.$el.tag).toBe('div')
+  })
 })
index 6766e276a0c56035f123be6515df6d2b2081ac2b..01717af2052ce0e34b70e671e429e3c535407048 100644 (file)
@@ -14,7 +14,8 @@ import {
   createRenderContext,
   exposePropsOnRenderContext,
   exposeSetupStateOnRenderContext,
-  ComponentPublicInstanceConstructor
+  ComponentPublicInstanceConstructor,
+  publicPropertiesMap
 } from './componentPublicInstance'
 import {
   ComponentPropsOptions,
@@ -169,7 +170,7 @@ export interface SetupContext<E = EmitsOptions> {
   attrs: Data
   slots: Slots
   emit: EmitFn<E>
-  expose: (exposed: Record<string, any>) => void
+  expose: (exposed?: Record<string, any>) => void
 }
 
 /**
@@ -291,6 +292,7 @@ export interface ComponentInternalInstance {
 
   // exposed properties via expose()
   exposed: Record<string, any> | null
+  exposeProxy: Record<string, any> | null
 
   /**
    * alternative proxy used only for runtime-compiled render functions using
@@ -447,6 +449,7 @@ export function createComponentInstance(
     render: null,
     proxy: null,
     exposed: null,
+    exposeProxy: null,
     withProxy: null,
     effects: null,
     provides: parent ? parent.provides : Object.create(appContext.provides),
@@ -837,7 +840,7 @@ export function createSetupContext(
     if (__DEV__ && instance.exposed) {
       warn(`expose() should be called only once per setup().`)
     }
-    instance.exposed = proxyRefs(exposed)
+    instance.exposed = exposed || {}
   }
 
   if (__DEV__) {
@@ -868,6 +871,23 @@ export function createSetupContext(
   }
 }
 
+export function getExposeProxy(instance: ComponentInternalInstance) {
+  if (instance.exposed) {
+    return (
+      instance.exposeProxy ||
+      (instance.exposeProxy = new Proxy(proxyRefs(markRaw(instance.exposed)), {
+        get(target, key: string) {
+          if (key in target) {
+            return target[key]
+          } else if (key in publicPropertiesMap) {
+            return publicPropertiesMap[key](instance)
+          }
+        }
+      }))
+    )
+  }
+}
+
 // record effects created during a component's setup() so that they can be
 // stopped when the component unmounts
 export function recordInstanceBoundEffect(
index c6398378ec8bbdb2fd888edd2f377a36eb31b8ee..5c7960d341c15a0009a1e43a7f1039dd57bf37cb 100644 (file)
@@ -14,7 +14,6 @@ import {
   isString,
   isObject,
   isArray,
-  EMPTY_OBJ,
   NOOP,
   isPromise
 } from '@vue/shared'
@@ -45,9 +44,7 @@ import {
 import {
   reactive,
   ComputedGetter,
-  WritableComputedOptions,
-  proxyRefs,
-  toRef
+  WritableComputedOptions
 } from '@vue/reactivity'
 import {
   ComponentObjectPropsOptions,
@@ -540,7 +537,7 @@ export let shouldCacheAccess = true
 
 export function applyOptions(instance: ComponentInternalInstance) {
   const options = resolveMergedOptions(instance)
-  const publicThis = instance.proxy!
+  const publicThis = instance.proxy! as any
   const ctx = instance.ctx
 
   // do not cache property access on public proxy during state initialization
@@ -773,12 +770,15 @@ export function applyOptions(instance: ComponentInternalInstance) {
 
   if (isArray(expose)) {
     if (expose.length) {
-      const exposed = instance.exposed || (instance.exposed = proxyRefs({}))
+      const exposed = instance.exposed || (instance.exposed = {})
       expose.forEach(key => {
-        exposed[key] = toRef(publicThis, key as any)
+        Object.defineProperty(exposed, key, {
+          get: () => publicThis[key],
+          set: val => (publicThis[key] = val)
+        })
       })
     } else if (!instance.exposed) {
-      instance.exposed = EMPTY_OBJ
+      instance.exposed = {}
     }
   }
 
index 83ab4521e0d8342ed553524a9ae9101636f119c3..9206b974ba4fd347d73eeea3d08c2e217cbaf5cd 100644 (file)
@@ -221,22 +221,25 @@ const getPublicInstance = (
   return getPublicInstance(i.parent)
 }
 
-const publicPropertiesMap: PublicPropertiesMap = extend(Object.create(null), {
-  $: i => i,
-  $el: i => i.vnode.el,
-  $data: i => i.data,
-  $props: i => (__DEV__ ? shallowReadonly(i.props) : i.props),
-  $attrs: i => (__DEV__ ? shallowReadonly(i.attrs) : i.attrs),
-  $slots: i => (__DEV__ ? shallowReadonly(i.slots) : i.slots),
-  $refs: i => (__DEV__ ? shallowReadonly(i.refs) : i.refs),
-  $parent: i => getPublicInstance(i.parent),
-  $root: i => getPublicInstance(i.root),
-  $emit: i => i.emit,
-  $options: i => (__FEATURE_OPTIONS_API__ ? resolveMergedOptions(i) : i.type),
-  $forceUpdate: i => () => queueJob(i.update),
-  $nextTick: i => nextTick.bind(i.proxy!),
-  $watch: i => (__FEATURE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP)
-} as PublicPropertiesMap)
+export const publicPropertiesMap: PublicPropertiesMap = extend(
+  Object.create(null),
+  {
+    $: i => i,
+    $el: i => i.vnode.el,
+    $data: i => i.data,
+    $props: i => (__DEV__ ? shallowReadonly(i.props) : i.props),
+    $attrs: i => (__DEV__ ? shallowReadonly(i.attrs) : i.attrs),
+    $slots: i => (__DEV__ ? shallowReadonly(i.slots) : i.slots),
+    $refs: i => (__DEV__ ? shallowReadonly(i.refs) : i.refs),
+    $parent: i => getPublicInstance(i.parent),
+    $root: i => getPublicInstance(i.root),
+    $emit: i => i.emit,
+    $options: i => (__FEATURE_OPTIONS_API__ ? resolveMergedOptions(i) : i.type),
+    $forceUpdate: i => () => queueJob(i.update),
+    $nextTick: i => nextTick.bind(i.proxy!),
+    $watch: i => (__FEATURE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP)
+  } as PublicPropertiesMap
+)
 
 if (__COMPAT__) {
   installCompatInstanceProperties(publicPropertiesMap)
index 66319fce3b8b742217a6249089886517dc0880f6..1d1ebe77274d6f353fbdf503e2501889e4fafb96 100644 (file)
@@ -19,6 +19,7 @@ import {
   ComponentOptions,
   createComponentInstance,
   Data,
+  getExposeProxy,
   setupComponent
 } from './component'
 import {
@@ -335,7 +336,7 @@ export const setRef = (
 
   const refValue =
     vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT
-      ? vnode.component!.exposed || vnode.component!.proxy
+      ? getExposeProxy(vnode.component!) || vnode.component!.proxy
       : vnode.el
   const value = isUnmount ? null : refValue