Vue3 源码解析系列 - expose

341 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第19天,点击查看活动详情

前言

上一篇我们讲了 setup 函数的用法,知道了 setup 函数中接收了两个参数,一个是 props,一个是 context。而 context 暴露了一些方法,attrs、slots、emit、expose。其中的attrs、slots、emit在vue2中都比较常用,那 expose 这个方法是用来干什么的呢?我们今天就来研究一下。

用法

当我在封装一个组件的时候,如果觉得暴露出去的属性和方法太多了,那么可以使用这个方法,expose 方法可以限制公共实例可以访问的属性。也就是说限制父组件访问这个组件的属性。

// 子组件
import { ref } from 'vue'
export default {
  setup(props) {
    const count = ref(0)

    function increment() {
      count.value++
    }
    
    return { increment, count }
  }
}
// 父组件
<template>
  <div @click="handleClick">
    <h2>我是父组件!</h2>
    <Child ref="child"  />
  </div>
</template>
<script>
import { ref } from 'vue';
import Child from './Child.vue'

export default defineComponent({
  components: {
    Child
  },
  setup() {
    const child = ref(null)
    const handleClick = () => {
      child.value.increment();
      console.log(child.value.count);
    }
    return {
      child,
      handleClick
    }
  }
});
</script>

我们定义了一个子组件和父组件,子组件中有一个属性 count 和方法increment。父组件中有一个方法 handleClick,这个方法中调用了子组件的 increment 方法和打印出 count 属性。
目前情况下我们调用 handleClick 是没问题的,count 会被增加并被返回回来。但如果现在在子组件中改一下代码。

// 子组件
import { ref } from 'vue'
export default {
  setup(props, {expose}) {
    const count = ref(0)

    function increment() {
      count.value++
    }
    expose({
      increment
    })
    return { increment, count }
  }
}

这时候在调用 handleClick 会发现 increment 还是调用成功了,但是 count 我们却无法再访问。这就是 expose 的作用。

源码

expose 的源码在 packages/runtime-core/src/component.ts 中

export function createSetupContext(
  instance: ComponentInternalInstance
): SetupContext {
  const expose: SetupContext['expose'] = exposed => {
    if (__DEV__ && instance.exposed) {
      warn(`expose() should be called only once per setup().`)
    }
    instance.exposed = exposed || {}
  }
  let attrs: Data
  return {
    get attrs() {
      return attrs || (attrs = createAttrsProxy(instance))
    },
    slots: instance.slots,
    emit: instance.emit,
    expose
  }
}

可以看到 expose 方法很简单,只做了一件事情,就是把我们传过去的需要暴露的对象直接赋值给 instance.exposed,那为什么一句话就能实现这个功能呢?
我们再来看下这个方法 getExposeProxy

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

在这个方法中,首先判断 instance.exposed 是否存在,不存在的话就不进行返回,也就是说没有 instance.exposed 的话就能访问子组件的所有属性。
如果存在的话,设置代理,并在 getter 中判断,需要访问的 key 是否在 instance.exposed 或者 publicPropertiesMap 中,存在就返回。
在 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)

所以我们设置了 exposed 后,除了 exposed 里面的属性可以访问,也可以访问子组件的这些公共方法。