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无关。
(这不是一个完整的答案,但是它确实允许动画步骤按设计进行。如果有足够的时间,我认为这可以完全进行。请参阅我的评论以获取更多信息。) >
在
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 ,因为暗示您的答案。 - 跳转到特定的单元格尚未经过测试,可能有效,也可能无效。