- 前端
- 开发工具
【免费下载链接】beautiful-react-hooks
🔥 A collection of beautiful and (hopefully) useful React hooks to speed-up your components and hooks development 🔥
本篇技术指南围绕 beautiful-react-hooks 仓库中的 useResizeObserver 文档 展开,系统讲解如何借助浏览器原生 ResizeObserver API 以声明式 Hook 的方式异步监听指定 DOM 元素的尺寸变化并实时获取其 DOMRect 数据。读完本文,你将掌握useResizeObserver的完整用法、防抖参数调优方法、源码级生命周期原理与特性支持检测机制,并能在自己的 React 组件中直接落地使用。
一、为什么需要 useResizeObserver
在 Web 开发中,"容器尺寸变化"是一类非常常见但难以优雅处理的需求:自适应布局、图表重绘、富文本编辑器高度同步、懒加载占位、Canvas 分辨率适配等场景,都需要在元素大小改变时做出响应。
传统方案主要依赖window.resize事件,但它有两个明显缺陷:
- 只能感知窗口尺寸变化,无法感知单个元素的尺寸变化(例如侧边栏折叠、容器内内容撑开导致的尺寸变化);
- 需要手动计算元素尺寸并反复比对,代码繁琐且容易产生性能问题。
useResizeObserver正是为解决这类问题而生。它封装了浏览器原生 ResizeObserver API,正如 useResizeObserver 文档 所述,其核心价值在于:
- 异步监听:在渲染流水线之外异步观察指定 HTML Element 的 DOM Rect 变化,不阻塞主线程关键路径;
- 自动清理:组件卸载时自动销毁观察器(disconnect),无需开发者手动编写清理逻辑,避免内存泄漏。
二、安装与引入
useResizeObserver是 beautiful-react-hooks 内置 Hook 之一,随主包一起安装:
npm install beautiful-react-hooks从 package.json 的exports字段可以看到,该 Hook 支持按需导入的三种模块格式:
"./useResizeObserver": { "import": "./dist/esm/useResizeObserver.js", "require": "./dist/useResizeObserver.js", "types": "./dist/useResizeObserver.d.ts" }即既支持 ESM 的import语法,也支持 CommonJS 的require语法,并附带完整的 TypeScript 类型声明,可按如下方式引入:
import useResizeObserver from 'beautiful-react-hooks/useResizeObserver';在安装之前,请确认项目满足 package.json 中声明的 peerDependencies 版本要求:react >=18.2.0 <20.0.0、react-dom >=18.2.0 <20.0.0。
三、基本用法:监听元素尺寸变化
useResizeObserver的使用方式非常直观:将useRef创建的引用传入 Hook,Hook 会返回当前元素的 DOMRect 数据(在首次渲染、尚未触发任何尺寸变化时为undefined)。
以下示例完整来自 useResizeObserver 文档 的 Basic Usage 章节,展示了如何监听一个可缩放的文本域容器并实时展示其尺寸:
import { useRef } from 'react'; import { Input } from 'antd'; import useResizeObserver from 'beautiful-react-hooks/useResizeObserver'; const ResizeObserverExample = () => { const ref = useRef(); const DOMRect = useResizeObserver(ref); return ( <DisplayDemo title="useResizeObserver"> <div ref={ref}> <Input.TextArea value="Resize me" /> </div> {DOMRect && ( <ul style={{ margin: '20px 0 10px 0', textAlign: 'left', padding: 0 }}> <li>Box width: {DOMRect.width}</li> <li>Box height: {DOMRect.height}</li> <li>Box left: {DOMRect.left}</li> <li>Box right: {DOMRect.right}</li> <li>Box top: {DOMRect.top}</li> <li>Box bottom: {DOMRect.bottom}</li> </ul> )} </DisplayDemo> ); }; <ResizeObserverExample />关键点说明:
useRef()创建引用并绑定到目标元素:ref必须挂载到需要监听的 DOM 节点上(示例中是包裹Input.TextArea的<div>);DOMRect初始为undefined:因此渲染前需要先做判空(DOMRect && ...),这与源码中useState<DOMRectValues>()无初始值的设计一致;- 返回的 DOMRect 包含六个字段:
width、height、left、right、top、bottom,覆盖了元素的几何尺寸与相对位置信息。
值得注意的是,示例中监听的尺寸来自元素自身(contentRect),因此当文本域内容变化导致外层div尺寸改变时,Hook 会同步触发更新——这正是window.resize事件做不到的。
四、返回值详解:DOMRectValues 类型
从 useResizeObserver 文档 的 Types 章节可以看到,Hook 的返回值并非完整的DOMRectReadOnly,而是经过精挑细选的六个字段:
import { type RefObject } from 'react'; export type DOMRectValues = Pick<DOMRectReadOnly, 'bottom' | 'height' | 'left' | 'right' | 'top' | 'width'>; /** * Uses the ResizeObserver API to observe changes within the given HTML Element DOM Rect. * @param elementRef * @param debounceTimeout * @returns {undefined} */ declare const useResizeObserver: <TElement extends HTMLElement>(elementRef: RefObject<TElement>, debounceTimeout?: number) => DOMRectValues | undefined; export default useResizeObserver;类型签名解析:
| 组成 | 含义 |
|---|---|
<TElement extends HTMLElement> | 泛型参数,限定被监听元素必须是 HTMLElement 及其子类(如HTMLDivElement、HTMLTextAreaElement) |
elementRef: RefObject<TElement> | 必传参数,指向目标 DOM 元素的 ref 对象 |
debounceTimeout?: number | 可选参数,回调防抖延时(毫秒),不传则使用默认值 |
返回值DOMRectValues \| undefined | 六字段几何数据,初始渲染时为undefined |
DOMRectValues通过 TypeScript 的Pick工具类型从DOMRectReadOnly中挑选字段,保证类型安全的同时去掉了x、y、toJSON等不常用成员,让返回值更聚焦、更轻量。
五、防抖机制与自定义超时时间
元素尺寸在拖拽、动画等场景下会高频触发回调,若每次变化都触发 React 重渲染,会造成不必要的性能开销。因此useResizeObserver内部采用了防抖(debounce)回调,将连续发生的尺寸变化合并为一次最终状态更新。
默认超时时间(注意文档与源码的差异)
useResizeObserver 文档 的 Debounce timeout 章节称默认超时为250ms;而实际源码 src/useResizeObserver.ts 中,参数默认值定义为:
const useResizeObserver = <TElement extends HTMLElement> (elementRef: RefObject<TElement>, debounceTimeout: number = 100): DOMRectValues | undefined => {以当前仓库源码为准,默认防抖时间为 100ms。文档描述与实现存在出入,建议在实际项目中显式传入你期望的超时值,避免依赖默认行为。
自定义超时示例
文档展示了通过第二个参数覆盖默认超时的完整用法,例如设置为 1000ms:
import { useRef } from 'react'; import useResizeObserver from 'beautiful-react-hooks/useResizeObserver'; const ResizeObserverExample = () => { const ref = useRef(); const DOMRect = useResizeObserver(ref, 1000); return ( <DisplayDemo title="useResizeObserver"> <div ref={ref}> <Input.TextArea value="Resize me" /> </div> {DOMRect && ( <ul style={{ margin: '20px 0 10px 0', textAlign: 'left', padding: 0 }}> <li>Box width: {DOMRect.width}</li> <li>Box height: {DOMRect.height}</li> <li>Box left: {DOMRect.left}</li> <li>Box right: {DOMRect.right}</li> <li>Box top: {DOMRect.top}</li> <li>Box bottom: {DOMRect.bottom}</li> </ul> )} </DisplayDemo> ); }; <ResizeObserverExample />参数调优建议:
- 高频变化 + 对延迟不敏感(如拖拽预览、动画跟随):可适当增大超时值(如 250~1000ms),减少重渲染次数;
- 需要尽量实时反馈(如图表重绘):可传 0 或较小值,让状态更新更及时;测试用例 test/useResizeObserver.spec.js 中即使用了
useResizeObserver(refMock, 0)来加速验证流程; - 超时值本质上是
lodash.debounce的 wait 参数,语义与其他防抖工具一致。
六、源码级原理剖析
深入 src/useResizeObserver.ts 的实现,可以看到 Hook 内部经历了"特性检测 → 创建观察器 → 挂载观察目标 → 清理回收"四个阶段。
1. 特性检测与提前返回
const isSupported = isApiSupported('ResizeObserver') const observerRef = useRef<ResizeObserver | null>(null) const [DOMRect, setDOMRect] = useState<DOMRectValues>() if (isClient && !isSupported) { warnOnce(errorMessage) return undefined }isApiSupported('ResizeObserver')来自 src/shared/isAPISupported.ts,其实现为api in window,即在客户端环境中检测window.ResizeObserver是否存在;- 若在客户端但 API 不被支持,会调用
warnOnce输出一次性警告并直接返回undefined; warnOnce实现在 src/shared/warnOnce.ts,使用 Map 缓存已提示过的消息,保证同一警告只打印一次,避免刷屏;- 警告文案明确指出:该错误既可能源于浏览器不支持,也可能源于服务端渲染(SSR)环境下调用。
2. 挂载时创建防抖观察器
useEffect(() => { if (isSupported) { const fn = debounce((entries) => { const { bottom, height, left, right, top, width } = entries[0].contentRect setDOMRect({ bottom, height, left, right, top, width }) }, debounceTimeout) observerRef.current = new ResizeObserver(fn) return () => { fn.cancel() if (observerRef.current && isFunction(observerRef?.current?.disconnect)) { observerRef.current.disconnect() } } } return () => {} }, [])这里有几个值得注意的实现细节:
- 防抖来自 lodash:仓库依赖
lodash.debounce@^4.0.8(见 package.json),回调会在连续触发后等待debounceTimeout毫秒才真正执行; - 数据来源是
entries[0].contentRect:ResizeObserver回调会收到ResizeObserverEntry[]数组,代码取第一个条目的contentRect(即元素内容盒区域),解构出六个字段后写入 state; - 清理逻辑成对出现:
fn.cancel()取消尚未执行的防抖回调,disconnect()断开观察器与元素的关联,两者共同确保组件卸载后不会再有残留的异步更新; - 空依赖数组
[]:观察器只在挂载时创建一次,整个生命周期复用同一个实例,避免反复创建销毁。
3. 观察目标元素的绑定
useEffect(() => { if (isSupported && elementRef.current) { if (observerRef.current && isFunction(observerRef?.current?.observe)) { observerRef.current.observe(elementRef.current) } } }, [elementRef.current])- 第二个
useEffect依赖elementRef.current,当 ref 挂载到真实 DOM 节点后,调用ResizeObserver.observe(element)开始观察; - 通过
isFunction守卫(见 src/shared/isFunction.ts)确保observe方法存在再调用,增强了跨环境健壮性; - 由于依赖是
elementRef.current,当 ref 指向的节点切换时,观察目标也会自动更新。
4. 返回状态
最终 Hook 返回DOMRectstate,即最新的六字段几何数据,供组件渲染使用。整个过程完全声明式:开发者只需提供 ref,其余创建、观察、防抖、清理均由 Hook 内部完成。
七、特性支持与 SSR 注意事项
useResizeObserver对运行环境做了严格的前置判断,核心依据是 src/shared/isClient.ts 中的isClient常量:
const isClient = !!( typeof window !== 'undefined' && window.document && window.document.createElement )结合isApiSupported的检查逻辑,可以梳理出 Hook 在不同环境下的行为:
| 运行环境 | isClient | ResizeObserver存在 | 行为 |
|---|---|---|---|
| 现代浏览器(客户端) | true | 是 | 正常创建观察器并返回 DOMRect 数据 |
| 旧浏览器(客户端) | true | 否 | 输出一次性警告,返回undefined,不抛错 |
| SSR / Node 环境 | false | 否 | 条件不满足,直接返回undefined,避免引用window导致崩溃 |
这个设计让 Hook 可以安全地在同构应用中使用:服务端渲染时返回undefined,不会因访问不存在的window.ResizeObserver而报错;客户端水合后则正常开始观察。
八、测试用例如何验证行为
仓库为useResizeObserver编写了专门的测试文件 test/useResizeObserver.spec.js,从两个维度验证了核心行为,可作为理解 Hook 语义的补充参考:
- 正常路径:使用 test/mocks/ResizeObserver.mock.js 模拟原生
ResizeObserver,通过ResizeObserver.simulateResize()触发尺寸变化回调(注入contentRect: { bottom: 10, height: 10, ... }),随后验证 Hook 返回值从undefined变为包含几何数据的对象; - 不支持路径:删除全局
ResizeObserver后调用 Hook,验证console.warn被调用且返回值保持undefined。
测试中还利用promiseDelay(250)等待防抖回调执行完毕(见 test/utils/promiseDelay.js),佐证了防抖机制确实生效——尺寸变化并非立即写入 state,而是在超时结束后才更新。
九、典型应用场景与注意事项
综合文档与源码,useResizeObserver适合以下场景:
- 响应式图表/Canvas 组件:容器尺寸变化时重新绘制图形;
- 自适应编辑器:文本内容撑开容器时同步调整内部布局;
- 布局联动:侧边栏折叠、面板展开时联动调整其他模块;
- 懒加载与占位:元素进入可视区或尺寸变化时加载对应资源。
使用时的注意事项:
- 务必判空后再访问字段:首次渲染时返回
undefined,直接访问DOMRect.width会抛错; - ref 必须已挂载:如果
elementRef.current始终为空,观察不会生效; - 注意防抖带来的延迟感:默认超时较小(源码为 100ms),如需明显减少重渲染频率可显式调大;
- 依赖原生 API:需要目标浏览器支持
ResizeObserver,Hook 只负责封装与优雅降级,不会引入 polyfill; - 不要手动创建多个观察器:Hook 内部已通过
observerRef复用单一实例,外部无需也无法干预。
结语
useResizeObserver是 beautiful-react-hooks 中把原生ResizeObserverAPI 转化为声明式 React 能力的典型实现:useRef定位元素、防抖合并高频回调、useEffect管理观察器生命周期、特性检测实现 SSR 安全。通过本文的文档 + 源码 + 测试三重对照,你不仅能熟练使用它,也能透彻理解其内部运行机制,从而在自己的组件中写出更健壮的响应式逻辑。
- 前端
- 开发工具
【免费下载链接】beautiful-react-hooks
🔥 A collection of beautiful and (hopefully) useful React hooks to speed-up your components and hooks development 🔥
相关推荐
元素尺寸变化监测:beautiful-react-hooks的useResizeObserver hooks完全指南
元素尺寸变化监测:beautiful react hooks的useResizeObserver hooks完全指南 在现代前端开发中,实时监测DOM元素尺寸变
前端开发工具beautiful-react-hooks useGlobalEvent:为 window 事件监听编写声明式 React Hook
beautiful react hooks useGlobalEvent:为 window 事件监听编写声明式 React Hook useGlobalEven
前端开发工具react-use 之 useMeasure:基于 ResizeObserver 的响应式元素尺寸监听 Hook 实战指南
react use 之 useMeasure:基于 ResizeObserver 的响应式元素尺寸监听 Hook 实战指南 导读 useMeasure 是 re
前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考