始终只加载一个索引范围的数据

load always data for only one index range

抱歉,如果标题有些误导。我正在使用 InfiniteLoaderTable,问题是我想要加载的数据总数几乎总是很大。如果我在每次调用 loadMoreRows 时附加数据,我最终可能会在状态中存储超过 100000 个条目,我认为这对性能不利。

我想知道是否可以不每次都附加数据。我只尝试将日志和状态设置为仅从 startIndex 加载到 stopIndex,但每次滚动时都会多次调用 loadMoreRows

这是我到目前为止所拥有的,以及我上面提到的尝试

'use strict';

import React = require('react');
import _ = require('lodash');
import Immutable = require('immutable');
import moment = require('moment-timezone');
import {AutoSizer, InfiniteLoader, Table, Column} from 'react-virtualized';

interface Props {
    logEntries: Immutable.List<Immutable.Map<string, any>>;
    count: number;
    timezone: string;
    logLimit: number;
    loadedRowsMap: { [index: number]: number; };
    onLoadMoreRows: (param: {startIndex: number, stopIndex: number}) => Promise<any>;
}

class LogLoader extends React.Component<Props, {}> {
    render() {
        const {logEntries, count, logLimit} = this.props;
        const headers: {
            name: string;
            dataKey: string;
            width: number;
            cellDataGetter?: (param: {rowData: any}) => any;
        }[] = [
            { name: 'Time', dataKey: 'dtCreated', width: 95, cellDataGetter: this.renderTime.bind(this) },
            { name: 'Level', dataKey: 'levelname', width: 65 },
            { name: 'Message', dataKey: 'message', width: 70, cellDataGetter: this.renderMessage.bind(this) }
        ];

        return (
            <InfiniteLoader isRowLoaded={this.isRowLoaded.bind(this)}
                            loadMoreRows={this.props.onLoadMoreRows}
                            minimumBatchSize={logLimit}
                            rowCount={count} >
                {
                    ({onRowsRendered, registerChild}) => (
                        <AutoSizer disableHeight>
                            {
                                ({width}) => (
                                    <Table headerHeight={20}
                                           height={400}
                                           onRowsRendered={onRowsRendered}
                                           rowRenderer={this.rowRenderer}
                                           ref={registerChild}
                                           rowCount={count}
                                           className='log-entries'
                                           gridClassName='grid'
                                           headerClassName='header'
                                           rowClassName={this.getRowClassName.bind(this)}
                                           rowGetter={({index}) => logEntries.get(index)}
                                           rowHeight={this.calculateRowHeight.bind(this)}
                                           width={width} >
                                        {
                                            headers.map(({name, dataKey, cellDataGetter, width}) => 
                                                <Column label={name}
                                                        key={name}
                                                        className={`${name.toLowerCase()} column`}
                                                        dataKey={dataKey}
                                                        cellDataGetter={cellDataGetter || this.renderTableCell.bind(this)}
                                                        width={width} />
                                            )
                                        }
                                    </Table>
                                )
                            }
                        </AutoSizer>
                    )
                }
            </InfiniteLoader>
        );
    }

    private calculateRowHeight({index}) {
        const rowData = this.props.logEntries.get(index);
        if(!rowData) {
            return 0;
        }
        const msg = this.renderMessage({rowData});

        const div = document.createElement('div');
        const span = document.createElement('span');
        span.style.whiteSpace = 'pre';
        span.style.wordBreak = 'break-all';
        span.style.fontSize = '12px';
        span.style.fontFamily = 'monospace';
        span.style.display = 'table-cell';
        span.innerHTML = msg;

        div.appendChild(span);
        document.body.appendChild(div);
        const height = div.offsetHeight;
        document.body.removeChild(div);

        return height;
    }

    private rowRenderer(params: any) {
        const {key, className, columns, rowData, style} = params;
        if(!rowData) {
            return (
                <div className={className}
                     key={key}
                     style={style} >
                    Loading...
                </div>
            );
        }

        return (
            <div className={className}
                 key={key}
                 style={style} >
                {columns}
            </div>
        );
    }

    private renderTableCell({rowData, dataKey}) {
        if(!rowData) {
            return null;
        }

        return rowData.get(dataKey);
    }

    private renderMessage({rowData}) {
        if(!rowData) {
            return null;
        }

        return rowData.get('message');
    }

    private renderTime({rowData}) {
        if(!rowData) {
            return null;
        }

        return moment(rowData.get('dtCreated'))
            .tz(this.props.timezone)
            .format('HH:mm:ss.SSS');
    }

    private getRowClassName({index}) {
        const {logEntries} = this.props;
        const data = logEntries.get(index);

        if(data) {
            return `log-entry ${data.get('levelname').toLowerCase()}`;
        }

        return '';
    }

    private isRowLoaded({index}) {
        return !!this.props.loadedRowsMap[index];
    }
}

export = LogLoader;

这里是从 parent 组件

传递下来的 loadMoreRows
private loadMoreRows({startIndex, stopIndex}) {
    const {loadedRowsMap, logEntries} = this.state,
          indexRange = _.range(startIndex, stopIndex + 1),
          updatedLoadedRowsMap = {};

    indexRange.forEach(i => { updatedLoadedRowsMap[i] = STATUS_LOADING; });
    this.setState({ loadedRowsMap: updatedLoadedRowsMap, loading: true });

    return Api.scriptLogs(null, { id: this.props.id })
        .then(({body: [count, logs]}) => {
            indexRange.forEach(i => { updatedLoadedRowsMap[i] = STATUS_LOADED; });
            const newLogs = logEntries.splice((stopIndex - startIndex + 1), 0, ...logs).toJS();
            this.setState({
                count,
                logEntries: Immutable.fromJS(newLogs),
                loadedRowsMap: updatedLoadedRowsMap,
                loading: false
            });
        });
}

更新答案

理解您所看到的内容的关键涉及几件事。

  1. 首先,每次加载新行时,您都会清除示例中的 loadedRowsMap。 (加载第 200-399 行后,您将丢弃第 0-199 行。)
  2. 您在您的示例 (200) 中设置了一个非常大的 minimumBatchSize。这告诉 InfiniteLoader 它加载的每个行块应该——如果可能的话——覆盖至少 200 个项目的范围。
  3. InfiniteLoader 定义了一个 threshold prop that controls how far ahead it pre-loads rows. This defaults to 15,这意味着当您在未加载范围的 15 行内滚动时(例如,在您的情况下为 185),它将提前加载一个块 - 希望通过当用户滚动到该范围的剩余时间时,它将被加载。
  4. InfiniteLoader checks both ahead and behind by the threshold amount 因为用户可以向上滚动 向下并且任一方向都可能包含未加载的行。

这一切都会导致以下情况:

  1. 用户滚动直到加载新的行范围。
  2. 您的演示应用放弃了之前的范围。
  3. 用户再滚动一点,InfiniteLoader 检查前后 15 行以查看数据是否已双向加载(根据上述原因 4)。
  4. 数据似乎尚未加载 above/before,因此 InfiniteLoader 尝试在该方向上至少加载 minimumBatchSize 条记录(例如 0-199)

这个问题的解决方案可能有以下几点:

  • 不要丢弃您之前加载的数据。 (无论如何,它可能占用的内存最少,所以请保留它。)
  • 如果你必须把它扔掉——不要把它全部扔掉。在用户当前所在的范围内保留一些。 (足以确保 threshold 检查在两个方向上都是安全的。)

原回答

也许我误会了你,但这就是 minimumBatchSize 道具的用途。它配置 InfiniteLoader 以加载足够大的数据块,这样您就不会在用户缓慢滚动数据时收到大量重复的加载请求。如果用户快速滚动 - 你可能会。但是没有办法解决这个问题。

如果这对您来说有问题,我建议您使用去抖 and/or 节流方法来防止触发过多的 HTTP 请求。 Debounce 可以帮助您避免加载用户快速滚动过去(无论如何都不会看到)的行,而 throttle 可以帮助您避免并行发送过多的 HTTP 请求。

乍一看,您的 isRowLoaded 函数和 loadMoreRows 中完成的映射看起来是正确的。但您可能还想验证这一点。 InfiniteLoader 不是有状态的 - 所以它会一遍又一遍地询问相同的行,除非你让它知道它们已经加载或正在加载。