在 Vue 3 的组合式 API 中,setupContext 提供了一个 expose 函数,它是控制组件公共接口的关键工具。通过 expose,你可以精确地决定哪些属性和方法可以被父组件通过 $refs 或模板引用(template refs)访问,从而实现更好的封装和 API 设计。
一、默认行为:setup 返回什么,就暴露什么
在 <script setup> 中,如果你不使用 defineExpose(其底层就是 expose),组件会默认暴露 setup 函数返回的所有内容。
<!-- Child.vue -->
<script setup>
import { ref } from 'vue';
const count = ref(0);
const message = 'Hello';
// 默认暴露 count 和 message
</script><!-- 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. 基本用法
<!-- 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>现在,父组件只能访问 count 和 updateCount:
const accessChild = () => {
console.log(child.value.count.value); // ✅ 可以访问
child.value.updateCount(); // ✅ 可以调用
console.log(child.value.internalMessage); // ❌ undefined
};2. 暴露计算属性和方法
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返回的所有属性 + 组件的data、methods、computed等(选项式 API 部分)。 - 使用
expose后:公共实例仅包含expose函数指定的对象。
2. 内部机制(简化版)
// 伪代码: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,甚至可以多次调用(后面的会覆盖前面的)。
import { ref, expose, onMounted } from 'vue';
const privateData = ref('secret');
const publicData = ref('public');
// 初始暴露
expose({ publicData });
onMounted(() => {
// 组件挂载后,暴露更多内容
expose({
publicData,
getData: () => privateData.value // 可以暴露访问私有数据的方法
});
});2. 暴露方法以修改内部状态
const count = ref(0);
const increment = () => count.value++;
const decrement = () => count.value--;
// 只暴露操作方法,不直接暴露 count
expose({
increment,
decrement
// 不暴露 count
});父组件可以控制计数器,但无法直接修改 count.value。
3. 与 defineExpose 的关系
在 <script setup> 中,defineExpose 是 expose 的编译时宏:
<script setup>
import { ref } from 'vue';
const count = ref(0);
// defineExpose 是编译时指令,等效于 expose({ count })
defineExpose({
count
});
</script>defineExpose 会被编译为 expose({ count }) 调用。
五、最佳实践
始终使用
expose/defineExpose:- 即使你想暴露所有内容,也显式声明,提高代码可读性。
- 避免意外暴露内部状态。
最小化暴露:
- 只暴露父组件真正需要的属性和方法。
- 遵循“信息隐藏”原则。
暴露方法而非直接状态:
- 通过方法控制状态变更,便于添加验证、日志等逻辑。
避免暴露 ref 对象本身:
- 考虑暴露
.value或提供 getter 方法。
- 考虑暴露
// 而不是暴露整个 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 应用。