关于reactjs:如何使用react virtualized list对可扩展行的行高调整进行故障排除?

How to troubleshoot row height adjustment with expandable rows with react virtualized list?

我正在使用带有反应虚拟化列表的行中的可扩展面板(Material-UI),并且在高度自动调整方面遇到了问题。我已经阅读了几篇SO帖子以及在react-virtualized网站上关于动态行高的一些问题,但是我有一个特定的问题,即行高何时出现一个"不合时宜"的问题在面板展开/折叠后进行调整。

这是预期的行为:

  • "行"面板默认情况下展开。
  • 用户单击可展开面板行。
  • 行面板折叠。
  • 行高调整为面板折叠。
  • 这是第一次点击的实际行为:

  • "行"面板默认情况下展开。
  • 用户单击可展开面板行。
  • 行面板折叠。
  • 行高不适应面板折叠。
  • 但是,在随后的单击中,行高确实会调整,但是会变为"相反"状态,这会导致不一致-即,当单击行面板以再次展开时,行高会像调整到行高一样它崩溃了,反之亦然。因此,当面板折叠时,其后会有一堆空白,而从技术上讲,当面板展开时,行高太小而看不到内容。
  • 除了发布代码,并不确定在面板折叠/展开时是否会触发onRowClick(),我不确定还包括哪些其他信息。

    这是父组件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    import React, { Component } from 'react';
    import AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer';
    import List from 'react-virtualized/dist/commonjs/List';
    import { CellMeasurer, CellMeasurerCache } from 'react-virtualized/dist/commonjs/CellMeasurer';
    import EquipSummaryRow from './EquipSummaryRow';
    import './EquipSummary.css';

    class EquipSummary extends Component {
      constructor(props) {
        super(props);

        this.cache = new CellMeasurerCache({
          fixedWidth: true,
        });

        this.rowRenderer = this.rowRenderer.bind(this);
        this.getDatum = this.getDatum.bind(this);
        this.onRowClick = this.onRowClick.bind(this);
      }

      getDatum(index) {
        const list = this.props.equipData;

        return list[index];
      }

      saveRef = (ref) => this.containerNode = ref;

      saveListRef = (ref) => {
        this.list = ref;
      }

      componentDidUpdate() {
        console.log('component updated');
        this.cache.clearAll();
        this.list.recomputeRowHeights();
      }

      onRowClick(e, index) {
        e.preventDefault();
        this.cache.clear(index);
        this.list.recomputeRowHeights();
        this.list.forceUpdateGrid();
      }

      rowRenderer({ index, key, parent, style }) {
        const datum = this.getDatum(index);
        return (
         
            <CellMeasurer
              cache={this.cache}
              columnIndex={0}
              key={key}
              rowIndex={index}
              parent={parent}
            >
              {({ measure }) => (
                <EquipSummaryRow
                  onClick={(e, idx) => this.onRowClick(e, idx)}
                  measure={measure}
                  datum={datum}
                  index={index}
                />
              )}
            </CellMeasurer>
         
        );
      }

      render() {
        console.log('rendering..');
        return (
         
            <AutoSizer>
              {({ width, height }) => (
                <List
                  ref={this.saveListRef}
                  width={width}
                  height={height}
                  rowHeight={this.cache.rowHeight}
                  rowCount={this.props.equipData.length}
                  rowRenderer={this.rowRenderer}
                  deferredMeasurementCache={this.cache}
                  equipData={this.props.equipData}
                />
              )}
            </AutoSizer>
         
        );
      }
    }

    export default EquipSummary;

    这是代表行的组件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    import React, { Component } from 'react';
    import {
      Table,
      TableBody,
      TableHeader,
      TableHeaderColumn,
      TableRow,
      TableRowColumn,
    } from 'material-ui/Table';
    import { MuiThemeProvider } from 'material-ui/styles';
    import ExpansionPanel from '@material-ui/core/ExpansionPanel';
    import ExpansionPanelSummary from '@material-ui/core/ExpansionPanelSummary';
    import ExpansionPanelDetails from '@material-ui/core/ExpansionPanelDetails';
    import Typography from '@material-ui/core/Typography';


    class EquipSummaryRow extends Component {
      render() {
        const { datum } = this.props;

        return (
         
            <ExpansionPanel
              defaultExpanded
              onChange={e => this.props.onClick(e, this.props.index)}
            >
              <ExpansionPanelSummary expandIcon={|}>
                <Typography>{`${datum.type}      (id: ${datum.instance}, points: ${datum.points.length})`}</Typography>
              </ExpansionPanelSummary>
              <ExpansionPanelDetails>
                <Table>
                  <TableHeader
                    displaySelectAll={false}
                    adjustForCheckbox={false}
                  >
                    <TableRow>
                      <TableHeaderColumn>Device</TableHeaderColumn>
                      <TableHeaderColumn>Object ID</TableHeaderColumn>
                      <TableHeaderColumn>Type</TableHeaderColumn>
                      <TableHeaderColumn>Name</TableHeaderColumn>
                      <TableHeaderColumn>Description</TableHeaderColumn>
                      <TableHeaderColumn>Units</TableHeaderColumn>
                      <TableHeaderColumn>Value</TableHeaderColumn>
                    </TableRow>
                  </TableHeader>
                  <TableBody
                    displayRowCheckbox={false}
                  >
                    {datum.points.map((row, index) => (
                      <TableRow key={row.id}>
                        <TableRowColumn>{row.device}</TableRowColumn>
                        <TableRowColumn>{row.objectID}</TableRowColumn>
                        <TableRowColumn>{row.type}</TableRowColumn>
                        <TableRowColumn>{row.name}</TableRowColumn>
                        <TableRowColumn>{row.description}</TableRowColumn>
                        <TableRowColumn>{row.units}</TableRowColumn>
                        <TableRowColumn>{row.value}</TableRowColumn>
                      </TableRow>
                      ))}
                  </TableBody>
                </Table>
              </ExpansionPanelDetails>
            </ExpansionPanel>
         
        );
      }
    }

    export default EquipSummaryRow;

    这可能与我使用缓存的方式有关吗?我一直在打这个头,所以任何建议都值得赞赏!


    解决了我的问题。问题在于Material-UI可扩展面板具有动画折叠,因此在面板达到其展开/折叠形式之间存在延迟。 \\'onChange \\'事件立即触发,因此在进行动画时进行测量。我目前正在尝试找出在动画结束后触发测量的方法,但这与react-virtualized无关。


    (这不是一个完整的答案,但是它确实允许动画步骤按设计进行。如果有足够的时间,我认为这可以完全进行。请参阅我的评论以获取更多信息。) >

    List组件中,有一个选项可以传递不同的cellRangeRenderer。此cellRangeRenderer负责生成附加到每个单个单元格的style对象。默认的cellRangeRenderer使用绝对定位来完成此操作。我创建了一个修改后的cellRangeRenderer,它实际上并没有在style对象中设置任何有效的内容,而是为单元格生成了一个容器。容器使用绝对定位来显示单元格相对于滚动条的位置,但是在容器内部,每个单元格均按原样呈现。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    import React from 'react'

    /**
     * Default implementation of cellRangeRenderer used by Grid.
     * This renderer supports cell-caching while the user is scrolling.
     */

    export default function cellRangeRenderer({
      cellCache,
      cellRenderer,
      columnSizeAndPositionManager,
      columnStartIndex,
      columnStopIndex,
      deferredMeasurementCache,
      horizontalOffsetAdjustment,
      isScrolling,
      isScrollingOptOut,
      parent, // Grid (or List or Table)
      rowSizeAndPositionManager,
      rowStartIndex,
      rowStopIndex,
      styleCache,
      verticalOffsetAdjustment,
      visibleColumnIndices,
      visibleRowIndices,
    }) {
      const renderedCells = [];

      // Browsers have native size limits for elements (eg Chrome 33M pixels, IE 1.5M pixes).
      // User cannot scroll beyond these size limitations.
      // In order to work around this, ScalingCellSizeAndPositionManager compresses offsets.
      // We should never cache styles for compressed offsets though as this can lead to bugs.
      // See issue #576 for more.
      const areOffsetsAdjusted = columnSizeAndPositionManager.areOffsetsAdjusted() || rowSizeAndPositionManager.areOffsetsAdjusted();

      const canCacheStyle = !isScrolling && !areOffsetsAdjusted;
      let styledBuffer = false
      let bufferStyle, containerStyle

      for (let rowIndex = rowStartIndex; rowIndex <= rowStopIndex; rowIndex++) {
        const rowDatum = rowSizeAndPositionManager.getSizeAndPositionOfCell(rowIndex);

        for (let columnIndex = columnStartIndex; columnIndex <= columnStopIndex; columnIndex++) {
          const columnDatum = columnSizeAndPositionManager.getSizeAndPositionOfCell(columnIndex);
          const isVisible = columnIndex >= visibleColumnIndices.start && columnIndex <= visibleColumnIndices.stop && rowIndex >= visibleRowIndices.start && rowIndex <= visibleRowIndices.stop;
          const key = `${rowIndex}-${columnIndex}`;
          let style;
          // this is the part that bugs out when react-virtualized re-renders part of the what's-showing-now list, rather than the entire what's-showing-now list
          // I'm just grabbing the first cell and assuming it's coordinates are the top of the what's-showing-now list
          if (!styledBuffer) {
            styledBuffer = true
            bufferStyle = {
              position: 'absolute',
              top: 0,
              left: 0,
              height: rowDatum.offset + verticalOffsetAdjustment,
              width: columnDatum.offset + horizontalOffsetAdjustment,
            }
            containerStyle = {
              position: 'absolute',
              top: rowDatum.offset + verticalOffsetAdjustment,
              left: columnDatum.offset + horizontalOffsetAdjustment,
              height: 'auto',
              width: 'auto',
            }
          }

          // Cache style objects so shallow-compare doesn't re-render unnecessarily.
          if (canCacheStyle && styleCache[key]) {
            style = styleCache[key];
          } else if (deferredMeasurementCache && !deferredMeasurementCache.has(rowIndex, columnIndex)) {
          // In deferred mode, cells will be initially rendered before we know their size.
          // Don't interfere with CellMeasurer's measurements by setting an invalid size.
            // Position not-yet-measured cells at top/left 0,0,
            // And give them width/height of 'auto' so they can grow larger than the parent Grid if necessary.
            // Positioning them further to the right/bottom influences their measured size.
            style = {
              height: 'auto',
              left: 0,
              position: 'absolute',
              top: 0,
              width: 'auto'
            };
          } else {
            // I'd go with a completely empty object, but that breaks other parts of react-virtualized that rely, at least, on 'width' being defined
            style = {
              height: 'auto',
              width: 'auto',
            }
            styleCache[key] = style;
          }

          const cellRendererParams = {
            columnIndex,
            isScrolling,
            isVisible,
            key,
            parent,
            rowIndex,
            style
          };

          let renderedCell;

          // Avoid re-creating cells while scrolling.
          // This can lead to the same cell being created many times and can cause performance issues for"heavy" cells.
          // If a scroll is in progress- cache and reuse cells.
          // This cache will be thrown away once scrolling completes.
          // However if we are scaling scroll positions and sizes, we should also avoid caching.
          // This is because the offset changes slightly as scroll position changes and caching leads to stale values.
          // For more info refer to issue #395
          //
          // If isScrollingOptOut is specified, we always cache cells.
          // For more info refer to issue #1028
          if ((isScrollingOptOut || isScrolling) && !horizontalOffsetAdjustment && !verticalOffsetAdjustment) {
            if (!cellCache[key]) {
              cellCache[key] = cellRenderer(cellRendererParams);
            }

            renderedCell = cellCache[key];

            // If the user is no longer scrolling, don't cache cells.
            // This makes dynamic cell content difficult for users and would also lead to a heavier memory footprint.
          } else {
            renderedCell = cellRenderer(cellRendererParams);
          }

          if (renderedCell === null || renderedCell === false) {
            continue;
          }

          if (process.env.NODE_ENV !== 'production') {
            warnAboutMissingStyle(parent, renderedCell);
          }

          renderedCells.push(renderedCell);
        }
      }

      // This is where the new"magic" happens
      return [(
       
      ), (
       
          {renderedCells}
       
      )];
    }

    function warnAboutMissingStyle(parent, renderedCellParam) {
      let renderedCell = renderedCellParam
      if (process.env.NODE_ENV !== 'production') {
        if (renderedCell) {
          // If the direct child is a CellMeasurer, then we should check its child
          // See issue #611
          if (renderedCell.type && renderedCell.type.__internalCellMeasurerFlag) {
            renderedCell = renderedCell.props.children;
          }

          if (renderedCell && renderedCell.props && renderedCell.props.style === undefined && parent.__warnedAboutMissingStyle !== true) {
            parent.__warnedAboutMissingStyle = true;

            console.warn('Rendered cell should include style property for positioning.');
          }
        }
      }
    }

    此代码从npm软件包中分发的内容的副本开始(从某种程度上绕过babel编译步骤)开始。它至少具有以下问题:

    • 必须在列表(而不是网格)中使用它。网格将要求将单元正确放置在网格内(材料UI网格,而不是反应虚拟化网格),而不是直接扔在那里。
    • react-virtualized有一些优化,可以在列表的子部分上调用此方法,而不是渲染整个块(修复此问题的时间超出了我尝试进行此修复的时间范围)。如果解决了此问题,则新的cellRangeRenderer可以按原样正确运行约90%。
    • 因为可以展开一行然后滚动,所以行大小仍然需要CellMeasurer才能计算高度。因为我没有将高度应用于每个单独的单元格,所以我们需要以一种稍微更聪明的方式使用高度重新计算容器的"顶部"高度。仅当您完全滚动到显示扩展面板的部分时,此选项才会损坏。仅将高度应用于style对象可能就足够了,但这未经测试。编辑:您仍然需要将呼叫延迟到measure,因为暗示您的答案。
    • 跳转到特定的单元格尚未经过测试,可能有效,也可能无效。