09|组件实例暴露
第二章:组件模型与通信纵览 / 09。数据继续走 props/emits;只有“让这个具体实例立即做一件事”时,才考虑通过模板 ref 调用公开命令。
为什么 <script setup> 默认是关闭的
普通组件模板引用会得到组件公开实例。使用 <script setup> 时,顶层变量不会自动全部暴露给父级;这是封装的默认值。子组件必须用 defineExpose 明确列出父级可以调用的成员。
最小可运行焦点组件
<script setup lang="ts">
import { useTemplateRef } from 'vue'
const input = useTemplateRef<HTMLInputElement>('input')
function focus() {
input.value?.focus()
}
function selectAll() {
input.value?.select()
}
defineExpose({ focus, selectAll })
</script>
<template>
<input ref="input" type="search" aria-label="搜索" />
</template><script setup lang="ts">
import { useTemplateRef } from 'vue'
import SearchField from './SearchField.vue'
const searchField = useTemplateRef<InstanceType<typeof SearchField>>('searchField')
function focusSearch() {
searchField.value?.focus()
}
</script>
<template>
<SearchField ref="searchField" />
<button type="button" @click="focusSearch">聚焦搜索框</button>
<button type="button" @click="searchField?.selectAll()">全选</button>
</template>useTemplateRef 是本课程 Vue 3.5 基线的模板引用 API。字符串 'searchField' 必须和模板中的 ref="searchField" 一致;组件类型从 typeof SearchField 推导,父级只能看到 focus 与 selectAll。
一次调用经过哪些对象
父级按钮点击
→ searchField.value(SearchField 的公开实例)
→ 公开方法 focus()
→ 子组件私有 input ref
→ 原生 HTMLInputElement.focus()父级没有拿到原生 input,也不知道子组件内部是否以后改成第三方控件。公开的是“聚焦”能力,不是实现对象。
模板引用为什么总要考虑 null
在父组件 setup 执行时,子组件尚未挂载,searchField.value 是 null。若子组件位于 v-if 中,条件为假或组件卸载后它也会回到 null。因此命令通常由挂载后的用户操作触发,并使用可选链或显式判断。
若必须在首次挂载后自动聚焦,应在 onMounted 中调用:
import { onMounted } from 'vue'
onMounted(() => searchField.value?.focus())不要用 nextTick 随意掩盖装配时机;先确认组件是否真的应该已经渲染。
公开动作,不公开状态树
下面的暴露面让父级开始依赖子组件实现:
defineExpose({ input, draft, validationErrors })公开实例会像普通组件实例一样自动解包暴露的 ref,父级可能直接改写本应私有的状态。更稳妥的做法是公开 focus()、reset()、validate() 这类有语义的少量动作;可持续观察的数据仍通过 props、emits 或 v-model 建模。
| 需求 | 优先通道 |
|---|---|
| 父级给子级数据 | props |
| 子级报告事实 | emits |
| 双向受控值 | v-model |
| 父级替换内容结构 | slots |
| 操作某个具体实例 | expose + template ref |
defineExpose 的编译时边界
它是 <script setup> 编译宏,不需要导入。应在任何顶层 await 之前调用;在 await 之后才暴露的属性,父级实例可能拿不到。公开面应在组件初始化时就确定,异步结果通过公开方法内部处理或响应式输出表达。
失败路径:条件卸载后仍强制调用
searchField.value!.focus()非空断言只让 TypeScript 闭嘴,不会阻止 v-if 移除组件。切换条件后再调用会得到运行时错误。正确检查是让类型保留 null,并在用户可见流程中决定:忽略、先重新显示组件,还是提示当前不可用。
练习:最小公开面的验证码输入
实现 CodeInput.vue 与父级页面:
- 子组件内部只有一个文本
<input>,公开focus()与selectAll()两个方法。 - 不暴露 DOM ref、输入值或其他局部状态。
- 父级使用
useTemplateRef<InstanceType<typeof CodeInput>>,提供“聚焦”“全选”“显示/隐藏”三个按钮。 - 隐藏后调用聚焦不得报错;重新显示后可以再次聚焦。
- 把
defineExpose暂时删掉,确认类型和运行行为都表明父级不能调用私有方法,再恢复。
参考答案:查看第 2 章第 09 题参考答案。
通关自检:你能解释父级为何只能看到两个方法、引用何时为 null,以及为什么输入值不应通过 expose 读取。