Back

Refs 详解:框架如何处理 DOM 直接访问

Refs 详解:框架如何处理 DOM 直接访问

现代前端框架承诺提供一个声明式的世界,你只需描述 UI 应该是什么样子,框架会处理其余的事情。但有时你需要跳出这个模型,直接操作实际的 DOM。这就是 DOM refs 的用武之地。

无论你使用的是 React refs、Vue template refs、Angular ElementRef,还是 Svelte bind:this,每个主流框架都提供了直接访问 DOM 的逃生舱口。理解何时以及如何使用这些工具——同时不破坏框架的保证——是区分称职开发者和那些制造微妙、难以调试问题的开发者的关键。

核心要点

  • Refs 提供了受控的逃生舱口,用于在声明式模式不足时直接访问 DOM
  • 常见用例包括焦点管理、滚动、布局测量和第三方库集成
  • 每个框架对 refs 的实现方式不同:React 使用可变的 ref 对象,Vue 使用带有 defineExpose() 的 template refs,Angular 提供 ElementRefRenderer2,Svelte 使用 bind:this
  • Refs 仅在挂载后的客户端存在——在 SSR 期间始终使用生命周期检查来保护访问

为什么框架提供 Refs

框架通过抽象层管理 DOM。React 和 Vue 使用虚拟 DOM。Angular 使用变更检测。Svelte 将框架完全编译掉。这些抽象实现了高效的更新,但它们也意味着框架拥有 DOM 结构的所有权。

当声明式模式不足时,直接访问 DOM 就变得必要了。浏览器提供的某些 API 根本无法表达为状态到 UI 的映射。

直接访问 DOM 的常见用例

焦点管理位居榜首。在输入元素上调用 .focus() 需要对该元素的引用。无论如何操作状态都无法将光标移动到文本字段中。

滚动也面临类似的挑战。以编程方式滚动到特定元素或位置需要命令式的 DOM 调用。

测量布局需要直接访问。在元素存在于 DOM 中之前,你无法知道它的尺寸或位置。读取 getBoundingClientRect() 或与 ResizeObserverIntersectionObserver 集成都需要真实的节点引用。

第三方库集成通常需要 refs。像 D3、视频播放器或基于 canvas 的工具等库期望获得可以直接操作的 DOM 节点。

核心权衡

Refs 打破了声明式模型。当你获取一个 DOM 节点并直接操作它时,你是在框架的认知之外工作。这会在你的代码和渲染结构之间创建紧密耦合。

谨慎使用 refs。如果你发现自己为了解决一个可以通过 state 或 props 处理的问题而使用 ref,请重新考虑。Refs 应该保持作为逃生舱口,而不是主要工具。

框架特定的实现

React Refs

React 19 中,refs 可以作为普通 props 传递给函数组件。forwardRef 包装器不再是大多数用例的强制要求,这显著简化了组件组合。

在 React 19 中,回调 refs 可以返回清理函数,允许你在元素卸载时分离事件监听器或执行清理工作(旧版 refs 仍然接收 null 以保持向后兼容性)。请注意,严格模式可能在开发期间多次调用 ref 回调——你的代码应该优雅地处理这种情况。

React refs 是可变容器。更改 .current 不会触发重新渲染,这使它们成为存储 DOM 引用而不引起更新循环的理想选择。

Vue Template Refs

Vue 通过模板中的 ref 属性暴露 refs。在组合式 API 中,你使用 ref(null) 创建一个 ref,并在挂载后访问元素。

Vue 鼓励通过 defineExpose() 显式暴露组件内部。这可以防止意外耦合到实现细节,同时在需要时仍允许受控访问。

Angular ElementRef

Angular 提供 ElementRefRenderer2 用于 DOM 访问。文档明确将这些标记为最后手段的工具。使用 ElementRef 不会自动使 DOM 访问”安全”——你仍然绕过了 Angular 的抽象。Renderer2 主要帮助实现平台抽象(如 SSR),而不是安全性。

尽可能优先使用 Angular 的内置指令和绑定。将 ElementRef 保留给没有声明式替代方案的情况。

Svelte bind:this

Svelte 使用 bind:this 来捕获元素引用。绑定在组件挂载后填充,这意味着你无法在初始脚本执行期间访问元素。

Svelte 中的 DOM 访问仅在挂载后的客户端发生,通常通过 onMount$effect(Svelte 5)。服务器端渲染生成 HTML 而不执行客户端绑定,因此 refs 在水合完成之前保持未定义状态。

SSR 和水合时机

在所有框架中,refs 仅在挂载后的客户端存在。在服务器端渲染期间,没有 DOM——只有 HTML 字符串。你的代码必须考虑到这一点。

使用生命周期检查来保护 ref 访问。在 React 中,在 effects 中访问 refs。在 Vue 中,使用 onMounted。在 Svelte 中,使用 onMount 或在水合后运行的响应式语句。在 SSR 期间尝试访问 refs 将产生未定义的值或错误。

何时使用 Refs

问问自己:这可以通过声明式方式解决吗?如果可以,避免使用 ref。如果浏览器 API 真正需要一个 DOM 节点——焦点、滚动、测量或集成——那么 refs 就是正确的工具。

保持 ref 使用的隔离性。将命令式逻辑包装在自定义 hooks 或 composables 中。这可以控制耦合,并使逃生舱口对阅读你代码的其他开发者显而易见。

结论

Refs 既没有被弃用也没有被劝阻。它们是每个框架设计中的刻意组成部分。有意识地使用它们,理解它们的权衡,你的应用程序将保持可维护性,同时仍能访问浏览器平台的全部功能。

常见问题

技术上可以,但你应该避免这样做。直接操作样式或内容会绕过框架的渲染周期,这可能导致组件状态和实际 DOM 之间的不一致。应该使用状态驱动的样式和内容更新。将 refs 保留给真正需要 DOM 节点的操作,如焦点、滚动或测量。

Refs 仅在组件挂载后填充。如果你在初始渲染期间或在服务器端渲染中访问 ref,它将是 null 或 undefined。始终使用生命周期钩子(如 React 中的 useEffect、Vue 中的 onMounted 或 Svelte 中的 onMount)来保护 ref 访问,以确保在与 DOM 元素交互之前它已存在。

通常不应该。Refs 会在组件之间创建紧密耦合,并绕过标准数据流。优先使用 props 和回调进行父子通信。仅在需要直接访问 DOM 时使用 refs,例如触发子元素的焦点或滚动。在 Vue 中,使用 defineExpose 来控制子组件暴露的内容。

概念相似,但实现方式不同。React 使用带有 current 属性的可变 ref 对象。Vue 使用通过组合式 API 访问的 template refs。Angular 提供 ElementRef 和 Renderer2。Svelte 使用 bind:this 指令。每种方法都反映了框架的架构,因此请查阅特定框架的文档以了解正确用法。

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay