Skip to content

在 Vue 3 的组合式 API 中,setupContext 提供了一个 expose 函数,它是控制组件公共接口的关键工具。通过 expose,你可以精确地决定哪些属性和方法可以被父组件通过 $refs 或模板引用(template refs)访问,从而实现更好的封装和 API 设计。


一、默认行为:setup 返回什么,就暴露什么

<script setup> 中,如果你不使用 defineExpose(其底层就是 expose),组件会默认暴露 setup 函数返回的所有内容。

vue
<!-- Child.vue -->
<script setup>
import { ref } from 'vue';

const count = ref(0);
const message = 'Hello';

// 默认暴露 count 和 message
</script>
vue
<!-- Parent.vue -->
<template>
  <Child ref="child" />
  <button @click="accessChild">Access Child</button>
</template>

<script setup>
import { ref } from 'vue';
import Child from './Child.vue';

const child = ref(null);

const accessChild = () => {
  console.log(child.value.count.value); // 1
  console.log(child.value.message);     // 'Hello'
};
</script>

问题message 是一个静态字符串,可能只是内部实现细节,不应该暴露给父组件。


二、使用 expose() 控制暴露内容

expose 函数允许你显式地指定哪些属性可以被外部访问。

1. 基本用法

vue
<!-- Child.vue -->
<script setup>
import { ref, expose } from 'vue';

const count = ref(0);
const internalMessage = 'Internal'; // 不应暴露
const updateCount = () => count.value++;

// 显式暴露
expose({
  count,
  updateCount
  // internalMessage 不在暴露列表中
});
</script>

现在,父组件只能访问 countupdateCount

js
const accessChild = () => {
  console.log(child.value.count.value);   // ✅ 可以访问
  child.value.updateCount();              // ✅ 可以调用
  console.log(child.value.internalMessage); // ❌ undefined
};

2. 暴露计算属性和方法

js
const doubled = computed(() => count.value * 2);
const reset = () => count.value = 0;

expose({
  count,
  doubled,
  updateCount,
  reset
});

父组件可以访问 doubled.value 和调用 reset()


三、expose 如何影响 $refs 的访问?

$refs(或模板 ref)引用的是组件实例的公共实例(public instance),而 expose 直接决定了这个公共实例的内容。

1. 组件公共实例的构成

  • 默认:组件的公共实例包含 setup 返回的所有属性 + 组件的 datamethodscomputed 等(选项式 API 部分)。
  • 使用 expose:公共实例仅包含 expose 函数指定的对象。

2. 内部机制(简化版)

js
// 伪代码:setup 执行后
const setupResult = setup(props, ctx);
let exposed = null;

if (ctx.expose) {
  // 如果调用了 expose
  const exposedObject = ctx.expose(); // 用户定义的暴露对象
  exposed = proxyExpose(exposedObject); // 创建代理
} else {
  // 未调用 expose,暴露 setup 返回的所有内容
  exposed = setupResult;
}

// 最终,$refs 指向的就是这个 exposed 对象
instance.exposed = exposed;

3. 实际影响

  • 未使用 expose

    • $refs 可以访问 setup 中定义的所有响应式变量、函数、甚至未使用的内部状态。
    • 封装性差,父组件可能依赖内部实现。
  • 使用 expose

    • $refs 只能访问 expose 列出的属性。
    • 你可以隐藏内部实现细节,只暴露必要的 API。
    • 封装性强,API 更清晰、更稳定。

四、expose 的高级用法

1. 动态暴露

你可以在 setup 的任何地方调用 expose,甚至可以多次调用(后面的会覆盖前面的)。

js
import { ref, expose, onMounted } from 'vue';

const privateData = ref('secret');
const publicData = ref('public');

// 初始暴露
expose({ publicData });

onMounted(() => {
  // 组件挂载后,暴露更多内容
  expose({
    publicData,
    getData: () => privateData.value // 可以暴露访问私有数据的方法
  });
});

2. 暴露方法以修改内部状态

js
const count = ref(0);

const increment = () => count.value++;
const decrement = () => count.value--;

// 只暴露操作方法,不直接暴露 count
expose({
  increment,
  decrement
  // 不暴露 count
});

父组件可以控制计数器,但无法直接修改 count.value

3. 与 defineExpose 的关系

<script setup> 中,defineExposeexpose 的编译时宏:

vue
<script setup>
import { ref } from 'vue';

const count = ref(0);

// defineExpose 是编译时指令,等效于 expose({ count })
defineExpose({
  count
});
</script>

defineExpose 会被编译为 expose({ count }) 调用。


五、最佳实践

  1. 始终使用 expose / defineExpose

    • 即使你想暴露所有内容,也显式声明,提高代码可读性。
    • 避免意外暴露内部状态。
  2. 最小化暴露

    • 只暴露父组件真正需要的属性和方法。
    • 遵循“信息隐藏”原则。
  3. 暴露方法而非直接状态

    • 通过方法控制状态变更,便于添加验证、日志等逻辑。
  4. 避免暴露 ref 对象本身

    • 考虑暴露 .value 或提供 getter 方法。
js
// 而不是暴露整个 ref
expose({ count }); // 父组件可随意修改 count.value

// 更好的做法
expose({
  getCount: () => count.value,
  setCount: (val) => { /* 验证 */ count.value = val; }
});

六、总结

expose 是 Vue 3 中控制组件公共 API 的强大工具:

  • 作用:显式定义组件实例通过 $refs 可访问的属性和方法。
  • 机制:它直接设置组件的 exposed 属性,覆盖默认行为。
  • 影响:决定了 $refs 引用对象的内容,实现精确的封装。
  • 推荐:在 <script setup> 中使用 defineExpose 显式暴露接口。

通过 expose,你可以将组件设计为一个“黑盒”,只暴露必要的“控制面板”,从而构建出更健壮、更可维护的 Vue 应用。