先记住这个答案
setup 执行时组件还没有完成首次渲染,模板 ref 通常是 null;onMounted 运行时,组件自己的节点树已创建并插入父容器,同步子组件也已挂载,因此适合读取或操作对应节点。模板条件为假时 ref 仍可能为空,组件在异步等待期间也可能卸载,所以代码仍需判空。onMounted 不在服务端渲染期间执行,也不保证图片、字体、动画或异步子组件已经完成。
- setup 时节点尚未创建与绑定
- onMounted 后仍要处理条件为空
- 后续节点变化要用相应更新时机
setup 先于首次节点创建
组合式初始化需要先准备状态与渲染逻辑,框架随后才能生成节点并把模板 ref 指向目标。若在 setup 同步读取 element.value,看见 null 是正常生命周期状态,不是 ref 声明失败。
把节点查询改成 setTimeout 只是碰巧延后,无法说明组件是否仍存在或条件是否满足。与其猜时间,不如把首次节点操作登记在 onMounted,并在实际使用前检查引用。
在组件内部保存挂载结果
下例让组件渲染一个 span,onMounted 中把当前文本保存下来。setup 阶段明确记录 ref 为空;挂载后引用指向节点并读取 ready,整个过程无需全局查询选择器。
类型写成 HTMLElement 是浏览器组件的常见写法。测试中也可以用自定义渲染器提供等价节点对象来验证钩子与引用绑定顺序,但那不能替代需要布局、焦点或可见性的真实浏览器验收。
import { h, onMounted, ref } from 'vue'
const setupSawNull = { value: false }
const mountedText = ref<string | null>(null)
const DomProbe = {
setup() {
const element = ref<HTMLElement | null>(null)
setupSawNull.value = element.value === null
onMounted(() => {
mountedText.value = element.value?.textContent ?? null
})
return () => h('span', { ref: element }, 'ready')
}
}挂载 DomProbe 后,setupSawNull 为真,mountedText 为 ready。示例验证首次引用绑定,不声称 mounted 后引用永远非空;条件分支删除节点时它还会回到 null。
首次挂载和后续更新要分开
onMounted 对一个组件实例通常用于首次挂载。之后切换条件节点、改变文本或替换列表时,要在具体状态修改后等待 nextTick,或使用 post 监听、onUpdated 等与更新有关的时点。
如果目标在异步子组件内部,父 mounted 不能保证它已经解析并挂载。应让子组件通过公开引用、事件或 ready 状态表达完成,避免父级根据固定延迟去查询还不存在的后代节点。
容易答错的地方
- 用全局选择器绕过模板 ref
- 全局编号可能在多实例、Teleport 或测试环境中命中错误节点,也难随组件结束清理。优先由组件模板 ref 表达所有权,并在节点可能缺失时处理空值。
- 认为 mounted 就等于页面全部稳定
- 节点插入不代表图片加载、字体替换、浏览器绘制和动画完成。读取尺寸或截图前要根据真正依赖的资源选择加载事件、尺寸观察或绘制时机。
面试官还会怎么问?
使用 v-if 后模板 ref 什么时候变回 null?
条件分支被移除并完成相应更新后,引用会反映节点不存在。依赖它的逻辑应覆盖出现和消失两条路径,销毁旧的第三方实例,不能只处理首次 mounted。
聚焦输入框为什么还要 nextTick?
如果输入框首次就在模板中,mounted 后可聚焦;若点击后才通过条件渲染创建,应先修改显示状态,再等待这次更新刷新,然后判空并调用 focus。
SSR 中应该怎样避免访问 document?
把依赖浏览器节点的工作放入 onMounted 或明确的客户端分支,服务端渲染阶段只产生可序列化状态和 HTML。不要在模块顶层读取 document,因为导入本身可能发生在服务端。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。