Jelajahi Sumber

feat(expose): always expose $ instance properties on child refs

Evan You 4 tahun lalu
induk
melakukan
b0203a3092

+ 23 - 1
packages/runtime-core/__tests__/apiExpose.spec.ts

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

+ 23 - 3
packages/runtime-core/src/component.ts

@@ -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(

+ 8 - 8
packages/runtime-core/src/componentOptions.ts

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

+ 19 - 16
packages/runtime-core/src/componentPublicInstance.ts

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

+ 2 - 1
packages/runtime-core/src/renderer.ts

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