ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

让虚拟滚动列表自动撑满容器:react-virtualized AutoSizer 从监听链路到 Flex 适配

让虚拟滚动列表自动撑满容器:react-virtualized AutoSizer 从监听链路到 Flex 适配

让虚拟滚动列表自动撑满容器:react-virtualized AutoSizer 从监听链路到 Flex 适配

【免费下载链接】react-virtualizedReact components for efficiently rendering large lists and tabular data项目地址: https://gitcode.com/gh_mirrors/re/react-virtualized

拉宽浏览器窗口后,列表右侧留出一大片空白;把侧边栏收起来,列表又突然挤成窄条。虚拟滚动组件 List、Table、Grid 都要求你给出明确的 width / height 像素值,可响应式列表的这两个值恰恰取决于父容器。AutoSizer 就是 react-virtualized 里专门补上这个缺口的组件:它挂在父容器上,量出可用的宽高,再通过 children 函数交给子组件。

一句话机制:给容器装上量尺

把 AutoSizer 理解成"给父容器装了一把量尺":它渲染一个占位 div,负责测量父节点可用尺寸;你通过 children 函数拿到 width / height,再决定画什么。最小可运行的版本长这样:

import { AutoSizer } from 'react-virtualized'; export function AutoSizerDemo() { return ( <div style={{ height: '100vh', display: 'flex' }}> <AutoSizer> {({ width, height }) => ( <div style={{ width, height, background: '#eef' }}> {width} x {height} </div> )} </AutoSizer> </div> ); }

窗口怎么拉,占位 div 就跟着变,占满父容器。量尺是 AutoSizer 的全部工作,虚拟滚动的组件只负责拿到尺寸后正常渲染。

尺寸到底从哪来

这里先纠正一个常见误解:AutoSizer 用的不是 ResizeObserver,而是仓库内 vendored 的 detect-element-resize 算法(source/vendor/detectElementResize.js)。它会在挂载时往父节点里注入一个隐藏的测量 div,把父节点position: static改成relative,然后监听测量 div 的 scroll 事件——容器一变形,内部 trigger 就滚动,scroll 事件随之触发。精简后的链路是这样的:

// 挂载时 addResizeListener(parentNode, onResize); // 注入测量 div + 挂 scroll 监听 onResize(); // 首次主动量一次 // 每次触发 onResize = () => { const width = parentNode.offsetWidth - paddingLeft - paddingRight; const height = parentNode.offsetHeight - paddingTop - paddingBottom; if (sizeChanged(width, height)) setState({ width, height }); };

数据流就是一条直线:容器变形 → 测量 div 产生 scroll → 回调里读offsetWidth / offsetHeight并减去父级 padding →setState→ React 重渲染 →children({ width, height })拿到新值。顺带说,父级 padding 会被自动扣除,所以带内边距的容器不用你自己算。

与 List / Table 的三档组合

📐 第一档,一行式透传,行高固定时最常用,rowHeight传普通数字:

<AutoSizer> {({ width, height }) => ( <List width={width} height={height} rowCount={1200} rowHeight={40} rowRenderer={({ index, key, style }) => ( <div key={key} style={style}> task-{index} </div> )} /> )} </AutoSizer>

第二档,行高不固定但希望有测量缓存,用 CellMeasurer 包一层。差异只在两处:deferredMeasurementCache换成缓存实例、rowHeight换成缓存的取值函数,AutoSizer 那部分一行不动:

const cache = new CellMeasurerCache({ fixedWidth: true, minHeight: 40 }); <AutoSizer> {({ width, height }) => ( <List width={width} height={height} rowCount={500} deferredMeasurementCache={cache} rowHeight={cache.rowHeight} rowRenderer={({ index, key, parent, style }) => ( <CellMeasurer cache={cache} parent={parent} columnIndex={0} rowIndex={index}> {({ measure, registerChild }) => ( <div ref={registerChild} style={style} onDoubleClick={measure}> {`node-${index}`} </div> )} </CellMeasurer> )} /> )} </AutoSizer>

动态高度在 Grid / Table 上同理:new CellMeasurerCache({ fixedHeight: false }),配合 Grid 的rowHeight={cache.rowHeight}即可。第三档是表格,AutoSizer 照常包在最外层,Table 吃尺寸,Column 管列:

<AutoSizer> {({ width, height }) => ( <Table width={width} height={height} headerHeight={32} rowHeight={40} rowCount={data.length} rowGetter={({ index }) => data[index]}> <Column label="node" dataKey="id" width={120} /> <Column label="load" dataKey="load" width={120} /> <Column label="region" dataKey="region" width={160} /> </Table> )} </AutoSizer>

配置项速查

children是唯一必传项,签名({ width, height }) => ReactNode,AutoSizer 的全部产出就藏在这个参数里,子组件想拿多少尺寸自己挑。defaultHeight/defaultWidth决定首次渲染的尺寸,默认都是 0——做 SSR 时用它们给首帧一个合理兜底,挂载后立刻被真实测量值覆盖。disableHeight/disableWidth各自关掉一个维度的自动管理,判断依据是"这个维度要不要跟容器走":列表高度写死 480 但宽度要跟着拉,就disableHeight后自己写height={480},children 里只透传 width。onResize在每次尺寸变化后触发,参数同样是{ width, height },适合埋点、通知外部状态同步;不传时是空函数,无副作用。

布局适配的坑:Flex、Grid 与初始为 0

🔧 Flex 里 AutoSizer 撑爆。症状是列表把兄弟元素挤没、滚动区高度变成 0,或页面越拉越长。根因在源码里写得很直白:AutoSizer 的根 div 默认被设成width: 0; height: 0(见source/AutoSizer/AutoSizer.js中 render 的 outerStyle),而 flex 容器不会因为一个 0 尺寸的子项约束布局,内容又贪心地要占满可用空间,两者叠加就形成增长循环。修法是中间垫一个块级元素,让 flex 先给它定好尺寸,AutoSizer 再去填满这个块:

<div style={{ display: 'flex' }}> <aside style={{ width: 220 }}>sidebar</aside> <div style={{ flex: '1 1 auto', minWidth: 0 }}> <AutoSizer>{renderList}</AutoSizer> </div> </div>

Grid 中缺min-width: 0的坑更隐蔽。症状是宽表格或宽列表撑开网格轨道,出现横向溢出。根因是 grid item 默认min-width: auto,不会被内容压缩到 min-content 以下,轨道被撑宽,AutoSizer 测到的也是被挤压后的宽度,滚动区域直接被裁掉。修法是在放 AutoSizer 的网格子项上显式放行收缩:

.grid > .list-cell { min-width: 0; overflow: auto; }

还有一类经典问题:AutoSizer 一直报 0。它只会填满父级,不会反过来撑开父级,所以父级(或更上层)没有确定高度时,量出来的就是 0。最简单的诊断是给父级加个背景色看看真实尺寸;确认是高度缺失就补height: 100%或flex: 1,做 SSR 的话再用defaultHeight兜住首帧。

高频 resize 下的性能处理

拖窗口时 resize 是连续触发的,AutoSizer 每次变化都会setState,React 18 的自动批处理能挡住大部分,但你在onResize里如果还有自己的逻辑(上报、同步布局),就需要自己降频,我一般会包一层节流:

const notifyResize = throttle(({ width, height }) => { window.__layoutEventBus?.emit('list:resize', { width, height }); }, 100); <AutoSizer onResize={notifyResize}>{renderList}</AutoSizer>

另外提一句:React 18 的useSyncExternalStore给了尺寸订阅一个潜在替代方案——自己写个基于 ResizeObserver 的subscribe / getSnapshot封装成 hook,就能绕开 AutoSizer 的测量机制,获得对测量时机的完全控制。对现有 react-virtualized 项目来说这属于优化项,不必急着替换。

延伸资源

  • docs/AutoSizer.md:AutoSizer 完整属性表与官方 List 示例,查参数语义看它;
  • docs/usingAutoSizer.md:FAQ,Flex 嵌套、高度为 0、与 InfiniteLoader 组合的问法基本都有;
  • source/AutoSizer/AutoSizer.example.js与source/CellMeasurer/CellMeasurer.DynamicHeightList.example.js:想看虚拟滚动列表接 AutoSizer 的完整跑法,按这两个文件抄就行。

【免费下载链接】react-virtualizedReact components for efficiently rendering large lists and tabular data项目地址: https://gitcode.com/gh_mirrors/re/react-virtualized

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进