使用 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};
    }
  }