使用 react-virtualized 渲染长列表

导语

有个聊天对话框需要改造成虚拟列表,碰到了一些问题做下记录。

聊天对话框有以下特点:

  • 内容高度非固定(长度姑且可以认为固定)
  • 内容有多种类型,如文本、图片、投票等
  • 内容可以删除

react-virtualized 来实现上面的功能需要一些工作量。

细节

高度非固定

react-virtualized 有提供一些高阶组件来解决典型问题,这里我们使用 CellMeasurer

基本上是照着官网的例子改的

import React from 'react'; import { CellMeasurer, CellMeasurerCache, List } from 'react-virtualized'; // In this example, average cell height is assumed to be about 50px. // This value will be used for the initial `Grid` layout. // Width is not dynamic. const cache = new CellMeasurerCache({ defaultHeight: 50, fixedWidth: true }); function rowRenderer ({ index, isScrolling, key, parent, style }) { const source // This comes from your list data return ( {({ measure, registerChild }) => ( // 'style' attribute required to position cell (within parent List)
blabla
)}
); } function renderList (props) { return ( ); }

内容状态会变化

react-virtualized 内部会缓存每一列的内容,所以如果你按照上面例子来实现,必然会碰到点击没效果、图片重叠等问题。

解决方法是在状态更新后调用 measure 方法。举个图片的例子,当图片加载完成后再调用 measure 来重新计算高度

function rowRenderer ({ index, isScrolling, key, parent, style }) { const source // This comes from your list data return ( {({ measure, registerChild }) => ( // 'style' attribute required to position cell (within parent List)
)}
); }

内容可以删除

官网的例子没找到有删除的功能。如果直接在 React 里面删除一项数据,你会发现 中间空了一行 或者 高度塌陷 ...

这是因为 react-virtualized 内部的机制导致的:

  • 内部会缓存所有项的内容、高度
  • 缓存的 key 是 column+row

举个例子,如果第1到3项的高度为100、200、300,那么删掉第2项以后,第三项就占了200的高度,显然就不对了

react-virtualized 根本就不知道哪一行改变了,需要用户手动告诉它。 我们使用 List 提供的 recomputeRowHeights 来告诉它。

滚动到底部

List 有 scrollToIndex 属性,但是这个属性有个问题,滚动到同一个属性没有效果。

还有个可选的方法是 scrollToPosition,相同的行数也可以.

其他

滚动条

因为高度不固定,所以滚动条在渲染后才会知道真正高度。这里可以试试 measureAll 方法

高度计算

固定住 width,将 height 设为 auto,然后读 node.offsetHeight

_getCellMeasurements() { const {cache} = this.props; const node = this._child || 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; 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); // Reset after measuring to avoid breaking styles; see #660 if (styleWidth) { node.style.width = styleWidth; } if (styleHeight) { node.style.height = styleHeight; } return {height, width}; } else { return {height: 0, width: 0}; } }