Skip to content

09|组件实例暴露 ​

第二章:组件模型与通信纵览 / 09。数据继续走 props/emits;只有“让这个具体实例立即做一件事”时,才考虑通过模板 ref 调用公开命令。

为什么 <script setup> 默认是关闭的 ​

普通组件模板引用会得到组件公开实例。使用 <script setup> 时,顶层变量不会自动全部暴露给父级;这是封装的默认值。子组件必须用 defineExpose 明确列出父级可以调用的成员。

最小可运行焦点组件 ​

vue
<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>
vue
<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。

一次调用经过哪些对象 ​

text
父级按钮点击
  → searchField.value(SearchField 的公开实例)
  → 公开方法 focus()
  → 子组件私有 input ref
  → 原生 HTMLInputElement.focus()

父级没有拿到原生 input,也不知道子组件内部是否以后改成第三方控件。公开的是“聚焦”能力,不是实现对象。

模板引用为什么总要考虑 null ​

在父组件 setup 执行时,子组件尚未挂载,searchField.value 是 null。若子组件位于 v-if 中,条件为假或组件卸载后它也会回到 null。因此命令通常由挂载后的用户操作触发,并使用可选链或显式判断。

若必须在首次挂载后自动聚焦,应在 onMounted 中调用:

ts
import { onMounted } from 'vue'

onMounted(() => searchField.value?.focus())

不要用 nextTick 随意掩盖装配时机;先确认组件是否真的应该已经渲染。

公开动作,不公开状态树 ​

下面的暴露面让父级开始依赖子组件实现:

ts
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 之后才暴露的属性,父级实例可能拿不到。公开面应在组件初始化时就确定,异步结果通过公开方法内部处理或响应式输出表达。

失败路径:条件卸载后仍强制调用 ​

ts
searchField.value!.focus()

非空断言只让 TypeScript 闭嘴,不会阻止 v-if 移除组件。切换条件后再调用会得到运行时错误。正确检查是让类型保留 null,并在用户可见流程中决定:忽略、先重新显示组件,还是提示当前不可用。

练习:最小公开面的验证码输入 ​

实现 CodeInput.vue 与父级页面:

  • 子组件内部只有一个文本 <input>,公开 focus() 与 selectAll() 两个方法。
  • 不暴露 DOM ref、输入值或其他局部状态。
  • 父级使用 useTemplateRef<InstanceType<typeof CodeInput>>,提供“聚焦”“全选”“显示/隐藏”三个按钮。
  • 隐藏后调用聚焦不得报错;重新显示后可以再次聚焦。
  • 把 defineExpose 暂时删掉,确认类型和运行行为都表明父级不能调用私有方法,再恢复。

参考答案:查看第 2 章第 09 题参考答案。

通关自检:你能解释父级为何只能看到两个方法、引用何时为 null,以及为什么输入值不应通过 expose 读取。

用心记录每一步成长