React-virtualized: как использовать отдельный rowRenderer для измерения высоты - PullRequest
0 голосов
/ 04 августа 2020

Я пытаюсь отображать большие текстовые документы со сложной разметкой, используя виртуализированный компонент List. Документ разбивается на фрагменты текста различной длины, короче некоторого максимума. Компонент List отображает каждый из этих фрагментов в виде строки.

Поскольку я не могу использовать фиксированную высоту строк, потому что длина фрагментов различается, я хотел бы использовать CellMeasurer. Проблема в том, что синтаксический анализ, необходимый для генерации разметки для каждого фрагмента, является дорогостоящим - это одна из причин, по которой я хочу использовать реакцию-виртуализацию. Даже если все фрагменты отображаются в фоновом режиме, это все равно будет слишком медленным.

Поскольку разметка не влияет на высоту, я хотел бы использовать более простую функцию rowRenderer, которая отображает текст без разметки только для измерения строк , а затем предоставить отдельный и более полный rowRenderer для фактического рендеринга каждого фрагмента с разметкой. Есть ли способ сделать это?

Ответы [ 2 ]

0 голосов
/ 06 августа 2020

После некоторых проб и ошибок я нашел хороший способ сделать это - заставить функцию rowRenderer решать, каким способом визуализировать строку. Вы можете сделать это, проверив свойство _rowHeightCache в экземпляре CellMeasurerCache use use в вашем списке. Обратите внимание, что ключи _rowHeightCache принимают форму: «index -0», где index - это индекс строки.

Вот как вы можете настроить средство визуализации строки:

  TextRowRenderer(
    {
      key, // Unique key within array of rows
      index, // Index of row within collection
      // isScrolling, // The List is currently being scrolled
      style, // Style object to be applied to row (to position it)
      parent, // reference to List
    },
  ) {
    const { textArray } = this.props;

    // get the cached row height for this row
    const rowCache = this.listCache._rowHeightCache[`${index}-0`];

    // if it has been cached, render it using the normal, more expensive
    // version of the component, without bothering to wrap it in a
    // CellMeasurer (it's already been measured!)

    // Note that listCache.defaultHeight has been set to 0, to make the
    // the comparison easy
    if (rowCache !== null && rowCache !== undefined && rowCache !== 0) {
      return (
        <TextChunk
          key={key}
          chunk={textArray[index]}
          index={index}
          style={style}
        />
      );
    }

    // If the row height has not been cached (meaning it has not been
    // measured, return the text chunk component, but this time:
    // a) it's wrapped in CellMeasurer, which is configured with the
    //    the cache, and
    // b) it receives the prop textOnly={true}, which it tells it to
    //    to skip rendering the markup
    return (
      <CellMeasurer
        cache={this.listCache}
        columnIndex={0}
        key={key}
        parent={parent}
        rowIndex={index}
      >
        {() => {
          return (
            <TextChunk
              key={key}
              chunk={textArray[index]}
              index={index}
              style={style}
              textOnly
            />
          );
        }}
      </CellMeasurer>
    );
  }

Затем он передается компоненту List обычным способом:

          <List
            height={pageHeight}
            width={pageWidth}
            rowCount={textArray.length}
            rowHeight={this.listCache.rowHeight}
            rowRenderer={this.TextRowRenderer}
            deferredMeasurementCache={this.listCache}
            style={{ outline: 'none' }}
            ref={(r) => { this.listRef = r; }}
          />

Включив обратный вызов ссылки, мы теперь можем получить доступ к методу measureAllRows, который мы можем использовать для принудительного List для отрисовки всех строк заранее, чтобы получить высоту. Это обеспечит правильную работу полосы прокрутки, но даже с флагом textOnly может потребоваться больше времени. В моем случае, я считаю, что ожидание того стоит.

Вот как вы можете вызвать measureAllRows:

  componentDidUpdate() {
    const {
      textArray,
    } = this.props;

    // We will only run measureAllRows if they have not been
    // measured yet. An easy way to check is to see if the
    // last row is measured
    const lastRowCache = this
       .listCache
       ._rowHeightCache[`${textArray.length - 1}-0`];

    if (this.listRef
      || lastRowCache === null
      || lastRowCache === undefined
      || lastRowCache === 0) {
      try {
        this.listRef.measureAllRows();
      } catch {
        console.log('failed to measure all rows');
      }
    }
  }

Блок try-catch необходим, потому что если он пытается измерить перед построением List он выдаст ошибку выхода индекса.

ЗАКЛЮЧИТЕЛЬНЫЕ МЫСЛИ

Другой возможный способ сделать это - иметь Фактический компонент List зависит от кэшированных измерений, а не от функции rowRenderer. Вы можете отобразить массив текста как обычный список, заключив каждую строку в <CellMeasurer>. Когда кеш заполнен, вы затем визуализируете виртуальный List, настроенный с использованием предварительно заполненного кеша. Это избавит от необходимости вызывать measureAllRows в componentDidUpdate или обратный вызов useEffect.

0 голосов
/ 04 августа 2020

Это, по общему признанию, действительно хакерский и не лучший метод, но не могли бы вы сделать что-то вроде:

<List
    rowHeight={(index) => {
        const rowData = data[index];
        let div = document.getElementById('renderer');
        if (!div) {
            div = document.createElement('div');
            div.id = 'renderer';
        }
        ReactDOM.render(div, <Row>{rowData}</Row>);
        const height = div.offsetHeight;
        ReactDOM.unmountComponentAtNode(div);
        if (index === data.length - 1) {
            div.parentNode.removeChild(div);
        }
        return height;
    }}
/>

Или, на самом деле, у вас может быть два списка, один с visibility: hidden, где вы просто визуализируете каждую строку без разметки, получаете высоту и добавляете ее в массив. Как только длина массива равна длине ваших данных, вы больше не показываете его, а затем визуализируете другой с rowHeight={index => heights[index]}

...