We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
本文源码分析基于 v9.20.1 以及本文 demo 的测试环境:Macbook Pro(Core i7 2.2G, 16G), Chrome 69,React 16.4.1
在上一篇文章中,我简单分析了 react-virtualized 的 List 组件是怎么实现虚拟列表的,在文章的最后,留下了一个问题:怎么尽量避免元素内容重叠的问题?本篇将进行简单分析。
react-virtualized
react-virtualized 的 List 组件虽然存在上述所说的问题,但是它还是可以通过和其它组件的组合来做的更好,尽量避免在渲染图文场景下的元素内容重叠问题。
在 Rendering large lists with React Virtualized 一文中介绍了怎么通过 react-virtualized 来做长列表数据的渲染优化,并详细介绍通过 AutoSizer 和 CellMeasurer 组件来实现 List 组件对列表项动态高度的支持:
AutoSizer
CellMeasurer
这篇文章我们就分析一下这两个组件。
如果不使用 AutoSizer 组件,直接使用 List 组件可能如下:
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 组件不需要管理子组件的高度。
disableHeight
true
AutoSizer 的实现也比较简单,先看起 render 方法:
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 方法:
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 的实现:
createDetectElementResize
_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 组件的实现。
state
re-render
CellMeasurer 组件会根据自身的内容自动计算大小,需要配合 CellMeasurerCache 组件使用,这个组件主要缓存已计算过的 cell 元素的大小。
CellMeasurerCache
先修改一下代码,看看其使用方式:
import { List, AutoSizer, CellMeasurer, CellMeasurerCache } from "react-virtualized"; class App extends Component { constructor() { ... this.cache = new CellMeasurerCache({ fixedWidth: true, defaultHeight: 180 }); } ... }
首先,我们创建了 CellMeasurerCache 实例,并设置了两个属性:
然后,我们需要修改 List 组件的 renderRow 方法以及 List 组件:
renderRow
// ... 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
从 List 组件的文档看,并没有 deferredMeasurementCache 属性说明,但在上一篇文章分析过,List 组件的内部实现是基于 Grid 组件的:
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 元素的大小了:
_maybeMeasureCell
// 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 的时候均会调用。
invalidateCellSizeAfterRender
_deferredInvalidateColumnIndex
_deferredInvalidateRowIndex
_handleInvalidatedGridSize
recomputeGridSize
componentDidUpdate
从上文可以知道,如果子组件是函数,则调用的时候还会传递 measure 参数,其值是 _measure,实现如下:
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,其实现比较简单,如果你有兴趣了解,可以去查看下其源代码。
forceUpdate
至此,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 的时候就能对原先预估值的计算进行纠正,得到更精确的值。
_updateCachedColumnAndRowSizes
CellSizeAndPositionManager#getTotalSize
CellSizeAndPositionManager#_cellSizeGetter
demo的完整代码戳此:ReactVirtualizedList
List 组件通过和 AutoSizer 组件以及 CellMeasurer 组件的组合使用,很好的优化了 List 组件自身对元素动态高度的支持。但从上文分析可知,CellMeasurer 组件会在其初次挂载(mount)和更新(update)的时候通过 _maybeMeasureCell 方法去更新自身的大小,如果 cell 元素只是渲染纯文本,这是可以满足需求的,但 cell 元素是渲染图文呢?
mount
update
因为图片存在网络请求,因而在组件挂载和更新时,图片未必就一定加载完成了,因而此时获取到的节点大小是不准确的,就有可能导致内容重叠:
这种情况下,我们可以根据项目的实际情况做一些布局上的处理,比如去掉 border,适当增加 cell 元素的 padding 或者 margin 等(:blush::blush::blush:),这是有点取巧的方式,那不取的方式是将 CellMeasurer 的子组件换成函数。
border
padding
margin
上文已经说过,如果子组件是函数,则调用的时候会传递一个函数 measure 作为参数,这个函数所做的事情就是重新计算对应 cell 元素的大小,并使 Grid 组件 re-render。因而,我们可以将这个参数绑定到 img 的 onLoad 事件中,当图片加载完成时,就会重新计算对应 cell 元素的大小,此时,获取到的节点大小就是比较精确的值了:
img
onLoad
// ... 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
<本文完>
The text was updated successfully, but these errors were encountered:
👍
Sorry, something went wrong.
No branches or pull requests
前言
在上一篇文章中,我简单分析了
react-virtualized
的 List 组件是怎么实现虚拟列表的,在文章的最后,留下了一个问题:怎么尽量避免元素内容重叠的问题?本篇将进行简单分析。react-virtualized
的 List 组件虽然存在上述所说的问题,但是它还是可以通过和其它组件的组合来做的更好,尽量避免在渲染图文场景下的元素内容重叠问题。在 Rendering large lists with React Virtualized 一文中介绍了怎么通过 react-virtualized 来做长列表数据的渲染优化,并详细介绍通过
AutoSizer
和CellMeasurer
组件来实现 List 组件对列表项动态高度的支持:这篇文章我们就分析一下这两个组件。
AutoSizer
如果不使用
AutoSizer
组件,直接使用List
组件可能如下:使用
AutoSizer
组件之后,代码可能变成如下:因为
List
组件使用了一个固定高度,所以将AutoSizer
的disableHeight
设置成true
就相当于告诉AutoSizer
组件不需要管理子组件的高度。AutoSizer
的实现也比较简单,先看起render
方法:然后再看下
componentDidMount
方法:在
componentDidMount
方法中,主要创建了监听元素大小变化的监听器。createDetectElementResize
方法(源代码)是基于 javascript-detect-element-resize 实现的,针对 SSR 的支持更改了一些代码。接下来看下_onResize
的实现:_onResize
方法做的事就是计算元素新的高宽,并更新state
,触发re-render
。接下来看看CellMeasurer
组件的实现。CellMeasurer
CellMeasurer
组件会根据自身的内容自动计算大小,需要配合CellMeasurerCache
组件使用,这个组件主要缓存已计算过的 cell 元素的大小。先修改一下代码,看看其使用方式:
首先,我们创建了
CellMeasurerCache
实例,并设置了两个属性:然后,我们需要修改
List
组件的renderRow
方法以及List
组件:对于
List
组件有三个变动:rowHeight
属性的值变成this.cache.rowHeight
deferredMeasurementCache
属性,并且其值为CellMeasurerCache
的实例renderRow
方法返回的元素外用CellMeasurer
组件包裹了一层从
List
组件的文档看,并没有deferredMeasurementCache
属性说明,但在上一篇文章分析过,List
组件的内部实现是基于Grid
组件的:而
Grid
组件是拥有这个属性的,其值是CellMeasurer
实例,因而这个属性实际上是传递给了Grid
组件。回到
CellMeasurer
组件,其实现是比较简单的:上述代码非常简单,
render
方法只做子组件的渲染,并在组件挂载和更新的时候都去调用_maybeMeasureCell
方法,这个方法就会去计算 cell 元素的大小了:从上文可以知道,如果子组件是函数,则调用的时候还会传递
measure
参数,其值是_measure
,实现如下:recomputeGridSize
方法时 Grid 组件的一个公开方法,用于重新计算元素的大小,并通过forceUpdate
强制 re-render,其实现比较简单,如果你有兴趣了解,可以去查看下其源代码。至此,
CellMeasurer
组件的实现就分析完结了。如上文所说,CellMeasurer
组件要和CellMeasurerCache
组件搭配使用,因而接下来我们快速看下CellMeasurerCache
组件的实现:对于
_updateCachedColumnAndRowSizes
方法需要补充说明一点的是,通过上一篇文章的分析,我们知道在组件内不仅需要去计算总的列宽和行高的(CellSizeAndPositionManager#getTotalSize
方法) ,而且需要计算 cell 元素的大小(CellSizeAndPositionManager#_cellSizeGetter
方法)。在 cell 元素被渲染之前,用的是预估的列宽值或者行高值计算的,此时的值未必就是精确的,而当 cell 元素渲染之后,就能获取到其真实的大小,因而缓存其真实的大小之后,在组件的下次 re-render 的时候就能对原先预估值的计算进行纠正,得到更精确的值。总结
List
组件通过和AutoSizer
组件以及CellMeasurer
组件的组合使用,很好的优化了List
组件自身对元素动态高度的支持。但从上文分析可知,CellMeasurer
组件会在其初次挂载(mount
)和更新(update
)的时候通过_maybeMeasureCell
方法去更新自身的大小,如果 cell 元素只是渲染纯文本,这是可以满足需求的,但 cell 元素是渲染图文呢?因为图片存在网络请求,因而在组件挂载和更新时,图片未必就一定加载完成了,因而此时获取到的节点大小是不准确的,就有可能导致内容重叠:
这种情况下,我们可以根据项目的实际情况做一些布局上的处理,比如去掉
border
,适当增加 cell 元素的padding
或者margin
等(:blush::blush::blush:),这是有点取巧的方式,那不取的方式是将CellMeasurer
的子组件换成函数。上文已经说过,如果子组件是函数,则调用的时候会传递一个函数
measure
作为参数,这个函数所做的事情就是重新计算对应 cell 元素的大小,并使Grid
组件 re-render。因而,我们可以将这个参数绑定到img
的onLoad
事件中,当图片加载完成时,就会重新计算对应 cell 元素的大小,此时,获取到的节点大小就是比较精确的值了:<本文完>
参考
The text was updated successfully, but these errors were encountered: