内容简介:本文源码分析基于 v9.20.1 以及本文 demo 的测试环境:Macbook Pro(Core i7 2.2G, 16G), Chrome 69,React 16.4.1在
前言
本文源码分析基于 v9.20.1 以及本文 demo 的测试环境:Macbook Pro(Core i7 2.2G, 16G), Chrome 69,React 16.4.1
在 上一篇 文章中,我简单分析了 react-virtualized
的 List 组件是怎么实现虚拟列表的,在文章的最后,留下了一个问题:怎么尽量避免元素内容重叠的问题?本篇将进行简单分析。
react-virtualized
的 List 组件虽然存在上述所说的问题,但是它还是可以通过和其它组件的组合来做的更好, 尽量避免在渲染图文场景下的元素内容重叠问题。
在 Rendering large lists with React Virtualized 一文中介绍了怎么通过 react-virtualized 来做长列表数据的渲染优化,并详细介绍通过 AutoSizer
和 CellMeasurer
组件来实现 List 组件对列表项动态高度的支持:
- AutoSizer:可以自动调整其子组件大小(高度和宽度)的高阶组件
- CellMeasurer:会自动计算组件的大小(高度和宽度)
这篇文章我们就分析一下这两个组件。
AutoSizer
如果不使用 AutoSizer
组件,直接使用 List
组件可能如下:
<List width={rowWidth} height={750} rowHeight={rowHeight} rowRenderer={this.renderRow} rowCount={this.list.length} overscanRowCount={3} />
使用 AutoSizer
组件之后,代码可能变成如下:
<AutoSizer disableHeight> { ({width, height}) => ( <List width={width} height={750} rowHeight={rowHeight} rowRenderer={this.renderRow} rowCount={this.list.length} overscanRowCount={3} /> ) } </AutoSizer>
因为 List
组件使用了一个固定高度,所以将 AutoSizer
的 disableHeight
设置成 true
就相当于告诉 AutoSizer
组件不需要管理子组件的高度。
AutoSizer
的实现也比较简单,先看起 render
方法:
// source/AutoSizer/AutoSizer.js // ... render() { const { children, className, disableHeight, disableWidth, style, } = this.props; const {height, width} = this.state; // 外部 div 的样式,外部 div 不需要设置高宽 // 而内部组件应该使用被计算后的高宽值 // https://github.com/bvaughn/react-virtualized/issues/68 const outerStyle: Object = {overflow: 'visible'}; const childParams: Object = {}; if (!disableHeight) { outerStyle.height = 0; childParams.height = height; } if (!disableWidth) { outerStyle.width = 0; childParams.width = width; } return ( <div className={className} ref={this._setRef} style={{ ...outerStyle, ...style, }}> {children(childParams)} </div> ); } // ... _setRef = (autoSizer: ?HTMLElement) => { this._autoSizer = autoSizer; }; // ...
然后再看下 componentDidMount
方法:
// source/AutoSizer/AutoSizer.js // ... componentDidMount() { const {nonce} = this.props; // 这里的每一个条件都可能是为了修复某一个边界问题(edge-cases),如 #203 #960 #150 etc. if ( this._autoSizer && this._autoSizer.parentNode && this._autoSizer.parentNode.ownerDocument && this._autoSizer.parentNode.ownerDocument.defaultView && this._autoSizer.parentNode instanceof this._autoSizer.parentNode.ownerDocument.defaultView.HTMLElement ) { // 获取父节点 this._parentNode = this._autoSizer.parentNode; // 创建监听器,用于监听元素大小的变化 this._detectElementResize = createDetectElementResize(nonce); // 设置需要被监听的节点以及回调处理 this._detectElementResize.addResizeListener( this._parentNode, this._onResize, ); this._onResize(); } } // ...
在 componentDidMount
方法中,主要创建了监听元素大小变化的监听器。 createDetectElementResize
方法( 源代码 )是基于 javascript-detect-element-resize 实现的,针对 SSR 的支持更改了一些代码。接下来看下 _onResize
的实现:
// source/AutoSizer/AutoSizer.js // ... _onResize = () => { const {disableHeight, disableWidth, onResize} = this.props; if (this._parentNode) { // 获取节点的高宽 const height = this._parentNode.offsetHeight || 0; const width = this._parentNode.offsetWidth || 0; const style = window.getComputedStyle(this._parentNode) || {}; const paddingLeft = parseInt(style.paddingLeft, 10) || 0; const paddingRight = parseInt(style.paddingRight, 10) || 0; const paddingTop = parseInt(style.paddingTop, 10) || 0; const paddingBottom = parseInt(style.paddingBottom, 10) || 0; // 计算新的高宽 const newHeight = height - paddingTop - paddingBottom; const newWidth = width - paddingLeft - paddingRight; if ( (!disableHeight && this.state.height !== newHeight) || (!disableWidth && this.state.width !== newWidth) ) { this.setState({ height: height - paddingTop - paddingBottom, width: width - paddingLeft - paddingRight, }); onResize({height, width}); } } }; // ...
_onResize
方法做的事就是计算元素新的高宽,并更新 state
,触发 re-render
。接下来看看 CellMeasurer
组件的实现。
CellMeasurer
CellMeasurer
组件会根据自身的内容自动计算大小,需要配合 CellMeasurerCache
组件使用,这个组件主要缓存已计算过的 cell 元素的大小。
先修改一下代码,看看其使用方式:
import { List, AutoSizer, CellMeasurer, CellMeasurerCache } from "react-virtualized"; class App extends Component { constructor() { ... this.cache = new CellMeasurerCache({ fixedWidth: true, defaultHeight: 180 }); } ... }
首先,我们创建了 CellMeasurerCache
实例,并设置了两个属性:
- fixedWidth:表示 cell 元素是固定宽度的,但高度是动态的
- defaultHeight:未被渲染的 cell 元素的默认高度(或预估高度)
然后,我们需要修改 List
组件的 renderRow
方法以及 List
组件:
// ... renderRow({ index, key, style, parent }) { // 上一篇分析过,List 是 columnCount 为 1 的 Grid 组件, // 因而 columnIndex 是固定的 0 return ( <CellMeasurer key={key} cache={this.cache} parent={parent} columnIndex={0} rowIndex={index}> <div style={style} className="row"> { // 省略 } </div> </CellMeasurer> ); } // ... <AutoSizer disableHeight> { ({width, height}) => ( <List width={width} height={750} rowHeight={this.cache.rowHeight} deferredMeasurementCache={this.cache} rowRenderer={this.renderRow} rowCount={this.list.length} overscanRowCount={3} /> ) } </AutoSizer>
对于 List
组件有三个变动:
-
rowHeight
属性的值变成this.cache.rowHeight
- 新增了
deferredMeasurementCache
属性,并且其值为CellMeasurerCache
的实例 - 在
renderRow
方法返回的元素外用CellMeasurer
组件包裹了一层
从 List
组件的 文档 看,并没有 deferredMeasurementCache
属性说明,但在上一篇文章分析过, List
组件的内部实现是基于 Grid
组件的:
// source/List/List.js // ... render() { //... return ( <Grid {...this.props} autoContainerWidth cellRenderer={this._cellRenderer} className={classNames} columnWidth={width} columnCount={1} noContentRenderer={noRowsRenderer} onScroll={this._onScroll} onSectionRendered={this._onSectionRendered} ref={this._setRef} scrollToRow={scrollToIndex} /> ); } // ...
而 Grid
组件是拥有这个属性的,其值是 CellMeasurer
实例,因而这个属性实际上是传递给了 Grid
组件。
回到 CellMeasurer
组件,其实现是比较简单的:
// source/CellMeasurer/CellMeasurer.js // ... componentDidMount() { this._maybeMeasureCell(); } componentDidUpdate() { this._maybeMeasureCell(); } render() { const {children} = this.props; return typeof children === 'function' ? children({measure: this._measure}) : children; } // ...
上述代码非常简单, render
方法只做子组件的渲染,并在组件挂载和更新的时候都去调用 _maybeMeasureCell
方法,这个方法就会去计算 cell 元素的大小了:
// source/CellMeasurer/CellMeasurer.js // ... // 获取元素的大小 _getCellMeasurements() { // 获取 CellMeasurerCache 实例 const {cache} = this.props; // 获取组件自身对应的 DOM 节点 const node = findDOMNode(this); if ( node && node.ownerDocument && node.ownerDocument.defaultView && node instanceof node.ownerDocument.defaultView.HTMLElement ) { // 获取节点对应的大小 const styleWidth = node.style.width; const styleHeight = node.style.height; /** * 创建 CellMeasurerCache 实例时,如果设置了 fixedWidth 为 true, * 则 hasFixedWidth() 返回 true;如果设置了 fixedHeight 为 true, * 则 hasFixedHeight() 返回 true。两者的默认值都是 false * 将 width 或 heigth 设置成 auto,便于得到元素的实际大小 **/ if (!cache.hasFixedWidth()) { node.style.width = 'auto'; } if (!cache.hasFixedHeight()) { node.style.height = 'auto'; } const height = Math.ceil(node.offsetHeight); const width = Math.ceil(node.offsetWidth); // 获取到节点的实际大小之后,需要重置样式 // https://github.com/bvaughn/react-virtualized/issues/660 if (styleWidth) { node.style.width = styleWidth; } if (styleHeight) { node.style.height = styleHeight; } return {height, width}; } else { return {height: 0, width: 0}; } } _maybeMeasureCell() { const { cache, columnIndex = 0, parent, rowIndex = this.props.index || 0, } = this.props; // 如果缓存中没有数据 if (!cache.has(rowIndex, columnIndex)) { // 则计算对应元素的大小 const {height, width} = this._getCellMeasurements(); // 缓存元素的大小 cache.set(rowIndex, columnIndex, width, height); // 通过上一篇文章的分析,可以得知 parent 是 Grid 组件 // 更新 Grid 组件的 _deferredInvalidate[Column|Row]Index,使其在挂载或更新的时候 re-render if ( parent && typeof parent.invalidateCellSizeAfterRender === 'function' ) { parent.invalidateCellSizeAfterRender({ columnIndex, rowIndex, }); } } } // ...
_maybeMeasureCell
方法最后会调用 invalidateCellSizeAfterRender
,从方法的 源代码 上看,它只是更新了组件的 _deferredInvalidateColumnIndex
和 _deferredInvalidateRowIndex
的值,那调用它为什么会触发 Grid 的 re-render 呢?因为这两个值被用到的地方是在 _handleInvalidatedGridSize
方法中,从其 源代码 上看,它调用了 recomputeGridSize
方法(后文会提到这个方法)。而 _handleInvalidatedGridSize
方法是在组件的 componentDidMount
和 componentDidUpdate
的时候均会调用。
从上文可以知道,如果子组件是函数,则调用的时候还会传递 measure
参数,其值是 _measure
,实现如下:
// source/CellMeasurer/CellMeasurer.js // ... _measure = () => { const { cache, columnIndex = 0, parent, rowIndex = this.props.index || 0, } = this.props; // 计算对应元素的大小 const {height, width} = this._getCellMeasurements(); // 对比缓存中的数据 if ( height !== cache.getHeight(rowIndex, columnIndex) || width !== cache.getWidth(rowIndex, columnIndex) ) { // 如果不相等,则重置缓存 cache.set(rowIndex, columnIndex, width, height); // 并通知父组件,即 Grid 组件强制 re-render if (parent && typeof parent.recomputeGridSize === 'function') { parent.recomputeGridSize({ columnIndex, rowIndex, }); } } }; // ...
recomputeGridSize
方法时 Grid 组件的一个公开方法,用于重新计算元素的大小,并通过 forceUpdate
强制 re-render,其实现比较简单,如果你有兴趣了解,可以去查看下其 源代码 。
至此, CellMeasurer
组件的实现就分析完结了。如上文所说, CellMeasurer
组件要和 CellMeasurerCache
组件搭配使用,因而接下来我们快速看下 CellMeasurerCache
组件的实现:
// source/CellMeasurer/CellMeasurerCache.js // ... // KeyMapper 是一个函数,根据行索引和列索引返回对应数据的唯一 ID // 这个 ID 会作为 Cache 的 key // 默认的唯一标识是 `${rowIndex}-${columnIndex}`,见下文的 defaultKeyMapper type KeyMapper = (rowIndex: number, columnIndex: number) => any; export const DEFAULT_HEIGHT = 30; export const DEFAULT_WIDTH = 100; // ... type Cache = { [key: any]: number, }; // ... _cellHeightCache: Cache = {}; _cellWidthCache: Cache = {}; _columnWidthCache: Cache = {}; _rowHeightCache: Cache = {}; _columnCount = 0; _rowCount = 0; // ... constructor(params: CellMeasurerCacheParams = {}) { const { defaultHeight, defaultWidth, fixedHeight, fixedWidth, keyMapper, minHeight, minWidth, } = params; // 保存相关值或标记位 this._hasFixedHeight = fixedHeight === true; this._hasFixedWidth = fixedWidth === true; this._minHeight = minHeight || 0; this._minWidth = minWidth || 0; this._keyMapper = keyMapper || defaultKeyMapper; // 获取默认的高宽 this._defaultHeight = Math.max( this._minHeight, typeof defaultHeight === 'number' ? defaultHeight : DEFAULT_HEIGHT, ); this._defaultWidth = Math.max( this._minWidth, typeof defaultWidth === 'number' ? defaultWidth : DEFAULT_WIDTH, ); // ... } // ... hasFixedHeight(): boolean { return this._hasFixedHeight; } hasFixedWidth(): boolean { return this._hasFixedWidth; } // ... // 根据索引获取对应的列宽 // 可用于 Grid 组件的 columnWidth 属性 columnWidth = ({index}: IndexParam) => { const key = this._keyMapper(0, index); return this._columnWidthCache.hasOwnProperty(key) ? this._columnWidthCache[key] : this._defaultWidth; }; // ... // 根据行索引和列索引获取对应 cell 元素的高度 getHeight(rowIndex: number, columnIndex: number = 0): number { if (this._hasFixedHeight) { return this._defaultHeight; } else { const key = this._keyMapper(rowIndex, columnIndex); return this._cellHeightCache.hasOwnProperty(key) ? Math.max(this._minHeight, this._cellHeightCache[key]) : this._defaultHeight; } } // 根据行索引和列索引获取对应 cell 元素的宽度 getWidth(rowIndex: number, columnIndex: number = 0): number { if (this._hasFixedWidth) { return this._defaultWidth; } else { const key = this._keyMapper(rowIndex, columnIndex); return this._cellWidthCache.hasOwnProperty(key) ? Math.max(this._minWidth, this._cellWidthCache[key]) : this._defaultWidth; } } // 是否有缓存数据 has(rowIndex: number, columnIndex: number = 0): boolean { const key = this._keyMapper(rowIndex, columnIndex); return this._cellHeightCache.hasOwnProperty(key); } // 根据索引获取对应的行高 // 可用于 List/Grid 组件的 rowHeight 属性 rowHeight = ({index}: IndexParam) => { const key = this._keyMapper(index, 0); return this._rowHeightCache.hasOwnProperty(key) ? this._rowHeightCache[key] : this._defaultHeight; }; // 缓存元素的大小 set( rowIndex: number, columnIndex: number, width: number, height: number, ): void { const key = this._keyMapper(rowIndex, columnIndex); if (columnIndex >= this._columnCount) { this._columnCount = columnIndex + 1; } if (rowIndex >= this._rowCount) { this._rowCount = rowIndex + 1; } // 缓存单个 cell 元素的高宽 this._cellHeightCache[key] = height; this._cellWidthCache[key] = width; // 更新列宽或行高的缓存 this._updateCachedColumnAndRowSizes(rowIndex, columnIndex); } // 更新列宽或行高的缓存,用于纠正预估值的计算 _updateCachedColumnAndRowSizes(rowIndex: number, columnIndex: number) { if (!this._hasFixedWidth) { let columnWidth = 0; for (let i = 0; i < this._rowCount; i++) { columnWidth = Math.max(columnWidth, this.getWidth(i, columnIndex)); } const columnKey = this._keyMapper(0, columnIndex); this._columnWidthCache[columnKey] = columnWidth; } if (!this._hasFixedHeight) { let rowHeight = 0; for (let i = 0; i < this._columnCount; i++) { rowHeight = Math.max(rowHeight, this.getHeight(rowIndex, i)); } const rowKey = this._keyMapper(rowIndex, 0); this._rowHeightCache[rowKey] = rowHeight; } } // ... function defaultKeyMapper(rowIndex: number, columnIndex: number) { return `${rowIndex}-${columnIndex}`; }
对于 _updateCachedColumnAndRowSizes
方法需要补充说明一点的是,通过上一篇文章的分析,我们知道在组件内不仅需要去计算总的列宽和行高的( CellSizeAndPositionManager#getTotalSize
方法) ,而且需要计算 cell 元素的大小( CellSizeAndPositionManager#_cellSizeGetter
方法)。在 cell 元素被渲染之前,用的是预估的列宽值或者行高值计算的,此时的值未必就是精确的,而当 cell 元素渲染之后,就能获取到其真实的大小,因而缓存其真实的大小之后,在组件的下次 re-render 的时候就能对原先预估值的计算进行纠正,得到更精确的值。
demo的完整代码戳此: ReactVirtualizedList
总结
List
组件通过和 AutoSizer
组件以及 CellMeasurer
组件的组合使用,很好的优化了 List
组件自身对元素动态高度的支持。但从上文分析可知, CellMeasurer
组件会在其初次挂载( mount
)和更新( update
)的时候通过 _maybeMeasureCell
方法去更新自身的大小,如果 cell 元素只是渲染纯文本,这是可以满足需求的,但 cell 元素是渲染图文呢?
因为图片存在网络请求,因而在组件挂载和更新时,图片未必就一定加载完成了,因而此时获取到的节点大小是不准确的,就有可能导致内容重叠:
这种情况下,我们可以根据项目的实际情况做一些布局上的处理,比如去掉 border
,适当增加 cell 元素的 padding
或者 margin
等(
CellMeasurer
的子组件换成函数 。
上文已经说过,如果子组件是函数,则调用的时候会传递一个函数 measure
作为参数,这个函数所做的事情就是重新计算对应 cell 元素的大小,并使 Grid
组件 re-render。因而,我们可以将这个参数绑定到 img
的 onLoad
事件中,当图片加载完成时,就会重新计算对应 cell 元素的大小,此时,获取到的节点大小就是比较精确的值了:
// ... renderRow({ index, key, style, parent }) { // 上一篇分析过,List 是 columnCount 为 1 的 Grid 组件, // 因而 columnIndex 是固定的 0 return ( <CellMeasurer key={key} cache={this.cache} parent={parent} columnIndex={0} rowIndex={index}> { ({measure}) => ( <div style={style} className="row"> <div>{`${text}`}</div> <img src={src} onLoad={measure}> </div> ) } </CellMeasurer> ); } // ...
渲染图文demo的完整代码戳此: ReactVirtualizedList with image
<本文完>
参考
以上所述就是小编给大家介绍的《react-virtualized 组件的虚拟列表优化分析》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 使用 React + Rxjs 实现一个虚拟滚动组件
- react-virtualized 组件的虚拟列表实现
- 如何用 React + Rxjs 实现一个虚拟滚动组件?
- 开源 UI 库中,唯一同时实现了大表格虚拟化和树表格的 Table 组件 原 荐
- VMware虚拟机嵌套部署KVM虚拟机指南
- 虚拟化生态系统及实现从虚拟化走向云端
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Big Java Late Objects
Horstmann, Cay S. / 2012-2 / 896.00元
The introductory programming course is difficult. Many students fail to succeed or have trouble in the course because they don't understand the material and do not practice programming sufficiently. ......一起来看看 《Big Java Late Objects》 这本书的介绍吧!