ReactJS: Моделирование Двунаправленной Бесконечной Прокрутки



наше приложение использует бесконечную прокрутку для навигации по большим спискам гетерогенных элементов. Есть несколько морщин:




  • обычно наши пользователи имеют список из 10 000 элементов и должны прокручивать 3k+.

  • это богатые элементы, поэтому мы можем иметь только несколько сотен в DOM, прежде чем производительность браузера станет неприемлемой.

  • элементы разной высоты.

  • элементы могут содержать изображения и мы позволяем пользователю прыгать на конкретную дату. Это сложно, потому что пользователь может перейти к точке в списке, где нам нужно загрузить изображения над видовым окном, что будет толкать содержимое вниз при загрузке. Неспособность справиться с этим означает, что пользователь может перейти к дате, но затем будет перенесен на более раннюю дату.


известно, что неполные решения:




  • https://github.com/guillaumervls/react-infinite-scroll - Это просто " загрузить больше, когда мы ударили Нижний " компонент. Он не отбраковывает ни одного из DOM, поэтому он умрет на тысячах предметов.


  • http://blog.vjeux.com/2013/javascript/scroll-position-with-react.html - показывает, как сохранить и восстановить положение прокрутки при вставке в верхней или вставка в нижней части, но не оба вместе.



Я не ищу код для полного решения (хотя это было бы здорово.) Вместо этого я ищу "способ реагирования", чтобы смоделировать эту ситуацию. Является ли положение прокрутки состоянием или нет? Какое состояние я должен отслеживать, чтобы сохранить свою позицию в списке? Какое состояние мне нужно сохранить, чтобы я запускал новый рендеринг при прокрутке в нижней или верхней части того, что отображается?

733   3  

3 ответов:

это смесь бесконечной таблицы и бесконечного сценария прокрутки. Лучшая абстракция, которую я нашел для этого, заключается в следующем:

обзор

сделать <List> компонент, который принимает массив все дети. Поскольку мы не визуализируем их, это действительно дешево, чтобы просто выделить их и отбросить их. Если выделение 10k слишком велико, вы можете вместо этого передать функцию, которая принимает диапазон и возвращает элементы.

<List>
  {thousandelements.map(function() { return <Element /> })}
</List>

код List компонент отслеживает положение прокрутки и отображает только те дочерние элементы, которые находятся в поле зрения. Он добавляет большой пустой div В начале, чтобы подделать предыдущие элементы, которые не отображаются.

теперь интересная часть заключается в том, что один раз Element компонент визуализируется, вы измеряете его высоту и сохраняете его в своем List. Это позволяет вычислить высоту прокладки и узнать, сколько элементов должно отображаться в вид.

изображения

вы говорите, что когда изображение загружается, они заставляют все "прыгать" вниз. Решение это установить размеры изображения в тег img: <img src="..." width="100" height="58" />. Таким образом, браузер не должен ждать, чтобы загрузить его, прежде чем знать, какой размер он будет отображаться. Это требует некоторой инфраструктуры, но это действительно стоит того.

если вы не можете знать размер заранее, то добавьте onload слушатели к вашему изображению и когда он загружен, измерьте его отображаемый размер и обновите сохраненную высоту строки и компенсируйте положение прокрутки.

Прыжки на случайный элемент

Если вам нужно перейти на случайный элемент в списке, который потребует некоторого обмана с положением прокрутки, потому что вы не знаете размер элементов между ними. То, что я предлагаю вам сделать, это усреднить высоту элемента, которую вы уже вычислили, и перейти к позиции прокрутки последней известной высоты + (количество элементов * среднее).

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

Реагировать Специфика

необходимо указать ключ для всех визуализированных элементов, чтобы они были поддерживается по просмотру. Существует две стратегии: (1) иметь только n ключей (0, 1, 2, ... n) где n-максимальное количество элементов, которые можно отобразить и использовать их положение по модулю n. (2) имеют разные ключи для каждого элемента. Если все элементы имеют одинаковую структуру, хорошо использовать (1) для повторного использования их узлов DOM. Если они не используют (2).

у меня было бы только две части состояния реакции: индекс первого элемента и количество отображаемых элементов. Ток прокрутки положение и высота всех элементов будут непосредственно прикреплены к this. При использовании setState вы на самом деле делаете rerender, который должен произойти только при изменении диапазона.

вот пример http://jsfiddle.net/vjeux/KbWJ2/9/ бесконечного списка, используя некоторые из методов, которые я описываю в этом ответе. Это будет какая-то работа, но React-это определенно хороший способ реализовать бесконечный список :)

взгляните на http://adazzle.github.io/react-data-grid/index.html# Это выглядит как мощный и производительный datagrid с Excel-подобными функциями и ленивой загрузкой / оптимизированным рендерингом (для миллионов строк) с богатыми функциями редактирования (MIT licenced). Еще не пробовал в нашем проекте, но будет делать это довольно скоро.

отличный ресурс для поиска таких вещей, как это также http://react.скалы/ В этом случае поиск тегов полезный: http://react.rocks / tag / InfiniteScroll

я столкнулся с аналогичной проблемой для моделирования однонаправленной бесконечной прокрутки с разнородными высотами элементов и поэтому сделал пакет npm из моего решения:

https://www.npmjs.com/package/react-variable-height-infinite-scroller

и демо:http://tnrich.github.io/react-variable-height-infinite-scroller/

вы можете проверить исходный код для логики, но я в основном следовал рецепту @Vjeux изложенный выше ответ. Я еще не взялся за переход к конкретному пункту, но я надеюсь реализовать это в ближайшее время.

вот вкрапления того, что код в настоящее время выглядит так:

var React = require('react');
var areNonNegativeIntegers = require('validate.io-nonnegative-integer-array');

var InfiniteScoller = React.createClass({
  propTypes: {
    averageElementHeight: React.PropTypes.number.isRequired,
    containerHeight: React.PropTypes.number.isRequired,
    preloadRowStart: React.PropTypes.number.isRequired,
    renderRow: React.PropTypes.func.isRequired,
    rowData: React.PropTypes.array.isRequired,
  },

  onEditorScroll: function(event) {
    var infiniteContainer = event.currentTarget;
    var visibleRowsContainer = React.findDOMNode(this.refs.visibleRowsContainer);
    var currentAverageElementHeight = (visibleRowsContainer.getBoundingClientRect().height / this.state.visibleRows.length);
    this.oldRowStart = this.rowStart;
    var newRowStart;
    var distanceFromTopOfVisibleRows = infiniteContainer.getBoundingClientRect().top - visibleRowsContainer.getBoundingClientRect().top;
    var distanceFromBottomOfVisibleRows = visibleRowsContainer.getBoundingClientRect().bottom - infiniteContainer.getBoundingClientRect().bottom;
    var rowsToAdd;
    if (distanceFromTopOfVisibleRows < 0) {
      if (this.rowStart > 0) {
        rowsToAdd = Math.ceil(-1 * distanceFromTopOfVisibleRows / currentAverageElementHeight);
        newRowStart = this.rowStart - rowsToAdd;

        if (newRowStart < 0) {
          newRowStart = 0;
        } 

        this.prepareVisibleRows(newRowStart, this.state.visibleRows.length);
      }
    } else if (distanceFromBottomOfVisibleRows < 0) {
      //scrolling down, so add a row below
      var rowsToGiveOnBottom = this.props.rowData.length - 1 - this.rowEnd;
      if (rowsToGiveOnBottom > 0) {
        rowsToAdd = Math.ceil(-1 * distanceFromBottomOfVisibleRows / currentAverageElementHeight);
        newRowStart = this.rowStart + rowsToAdd;

        if (newRowStart + this.state.visibleRows.length >= this.props.rowData.length) {
          //the new row start is too high, so we instead just append the max rowsToGiveOnBottom to our current preloadRowStart
          newRowStart = this.rowStart + rowsToGiveOnBottom;
        }
        this.prepareVisibleRows(newRowStart, this.state.visibleRows.length);
      }
    } else {
      //we haven't scrolled enough, so do nothing
    }
    this.updateTriggeredByScroll = true;
    //set the averageElementHeight to the currentAverageElementHeight
    // setAverageRowHeight(currentAverageElementHeight);
  },

  componentWillReceiveProps: function(nextProps) {
    var rowStart = this.rowStart;
    var newNumberOfRowsToDisplay = this.state.visibleRows.length;
    this.props.rowData = nextProps.rowData;
    this.prepareVisibleRows(rowStart, newNumberOfRowsToDisplay);
  },

  componentWillUpdate: function() {
    var visibleRowsContainer = React.findDOMNode(this.refs.visibleRowsContainer);
    this.soonToBeRemovedRowElementHeights = 0;
    this.numberOfRowsAddedToTop = 0;
    if (this.updateTriggeredByScroll === true) {
      this.updateTriggeredByScroll = false;
      var rowStartDifference = this.oldRowStart - this.rowStart;
      if (rowStartDifference < 0) {
        // scrolling down
        for (var i = 0; i < -rowStartDifference; i++) {
          var soonToBeRemovedRowElement = visibleRowsContainer.children[i];
          if (soonToBeRemovedRowElement) {
            var height = soonToBeRemovedRowElement.getBoundingClientRect().height;
            this.soonToBeRemovedRowElementHeights += this.props.averageElementHeight - height;
            // this.soonToBeRemovedRowElementHeights.push(soonToBeRemovedRowElement.getBoundingClientRect().height);
          }
        }
      } else if (rowStartDifference > 0) {
        this.numberOfRowsAddedToTop = rowStartDifference;
      }
    }
  },

  componentDidUpdate: function() {
    //strategy: as we scroll, we're losing or gaining rows from the top and replacing them with rows of the "averageRowHeight"
    //thus we need to adjust the scrollTop positioning of the infinite container so that the UI doesn't jump as we 
    //make the replacements
    var infiniteContainer = React.findDOMNode(this.refs.infiniteContainer);
    var visibleRowsContainer = React.findDOMNode(this.refs.visibleRowsContainer);
    var self = this;
    if (this.soonToBeRemovedRowElementHeights) {
      infiniteContainer.scrollTop = infiniteContainer.scrollTop + this.soonToBeRemovedRowElementHeights;
    }
    if (this.numberOfRowsAddedToTop) {
      //we're adding rows to the top, so we're going from 100's to random heights, so we'll calculate the differenece
      //and adjust the infiniteContainer.scrollTop by it
      var adjustmentScroll = 0;

      for (var i = 0; i < this.numberOfRowsAddedToTop; i++) {
        var justAddedElement = visibleRowsContainer.children[i];
        if (justAddedElement) {
          adjustmentScroll += this.props.averageElementHeight - justAddedElement.getBoundingClientRect().height;
          var height = justAddedElement.getBoundingClientRect().height;
        }
      }
      infiniteContainer.scrollTop = infiniteContainer.scrollTop - adjustmentScroll;
    }

    var visibleRowsContainer = React.findDOMNode(this.refs.visibleRowsContainer);
    if (!visibleRowsContainer.childNodes[0]) {
      if (this.props.rowData.length) {
        //we've probably made it here because a bunch of rows have been removed all at once
        //and the visible rows isn't mapping to the row data, so we need to shift the visible rows
        var numberOfRowsToDisplay = this.numberOfRowsToDisplay || 4;
        var newRowStart = this.props.rowData.length - numberOfRowsToDisplay;
        if (!areNonNegativeIntegers([newRowStart])) {
          newRowStart = 0;
        }
        this.prepareVisibleRows(newRowStart , numberOfRowsToDisplay);
        return; //return early because we need to recompute the visible rows
      } else {
        throw new Error('no visible rows!!');
      }
    }
    var adjustInfiniteContainerByThisAmount;

    //check if the visible rows fill up the viewport
    //tnrtodo: maybe put logic in here to reshrink the number of rows to display... maybe...
    if (visibleRowsContainer.getBoundingClientRect().height / 2 <= this.props.containerHeight) {
      //visible rows don't yet fill up the viewport, so we need to add rows
      if (this.rowStart + this.state.visibleRows.length < this.props.rowData.length) {
        //load another row to the bottom
        this.prepareVisibleRows(this.rowStart, this.state.visibleRows.length + 1);
      } else {
        //there aren't more rows that we can load at the bottom so we load more at the top
        if (this.rowStart - 1 > 0) {
          this.prepareVisibleRows(this.rowStart - 1, this.state.visibleRows.length + 1); //don't want to just shift view
        } else if (this.state.visibleRows.length < this.props.rowData.length) {
          this.prepareVisibleRows(0, this.state.visibleRows.length + 1);
        }
      }
    } else if (visibleRowsContainer.getBoundingClientRect().top > infiniteContainer.getBoundingClientRect().top) {
      //scroll to align the tops of the boxes
      adjustInfiniteContainerByThisAmount = visibleRowsContainer.getBoundingClientRect().top - infiniteContainer.getBoundingClientRect().top;
      //   this.adjustmentScroll = true;
      infiniteContainer.scrollTop = infiniteContainer.scrollTop + adjustInfiniteContainerByThisAmount;
    } else if (visibleRowsContainer.getBoundingClientRect().bottom < infiniteContainer.getBoundingClientRect().bottom) {
      //scroll to align the bottoms of the boxes
      adjustInfiniteContainerByThisAmount = visibleRowsContainer.getBoundingClientRect().bottom - infiniteContainer.getBoundingClientRect().bottom;
      //   this.adjustmentScroll = true;
      infiniteContainer.scrollTop = infiniteContainer.scrollTop + adjustInfiniteContainerByThisAmount;
    }
  },

  componentWillMount: function(argument) {
    //this is the only place where we use preloadRowStart
    var newRowStart = 0;
    if (this.props.preloadRowStart < this.props.rowData.length) {
      newRowStart = this.props.preloadRowStart;
    }
    this.prepareVisibleRows(newRowStart, 4);
  },

  componentDidMount: function(argument) {
    //call componentDidUpdate so that the scroll position will be adjusted properly
    //(we may load a random row in the middle of the sequence and not have the infinte container scrolled properly initially, so we scroll to the show the rowContainer)
    this.componentDidUpdate();
  },

  prepareVisibleRows: function(rowStart, newNumberOfRowsToDisplay) { //note, rowEnd is optional
    //setting this property here, but we should try not to use it if possible, it is better to use
    //this.state.visibleRowData.length
    this.numberOfRowsToDisplay = newNumberOfRowsToDisplay;
    var rowData = this.props.rowData;
    if (rowStart + newNumberOfRowsToDisplay > this.props.rowData.length) {
      this.rowEnd = rowData.length - 1;
    } else {
      this.rowEnd = rowStart + newNumberOfRowsToDisplay - 1;
    }
    // var visibleRows = this.state.visibleRowsDataData.slice(rowStart, this.rowEnd + 1);
    // rowData.slice(rowStart, this.rowEnd + 1);
    // setPreloadRowStart(rowStart);
    this.rowStart = rowStart;
    if (!areNonNegativeIntegers([this.rowStart, this.rowEnd])) {
      var e = new Error('Error: row start or end invalid!');
      console.warn('e.trace', e.trace);
      throw e;
    }
    var newVisibleRows = rowData.slice(this.rowStart, this.rowEnd + 1);
    this.setState({
      visibleRows: newVisibleRows
    });
  },
  getVisibleRowsContainerDomNode: function() {
    return this.refs.visibleRowsContainer.getDOMNode();
  },


  render: function() {
    var self = this;
    var rowItems = this.state.visibleRows.map(function(row) {
      return self.props.renderRow(row);
    });

    var rowHeight = this.currentAverageElementHeight ? this.currentAverageElementHeight : this.props.averageElementHeight;
    this.topSpacerHeight = this.rowStart * rowHeight;
    this.bottomSpacerHeight = (this.props.rowData.length - 1 - this.rowEnd) * rowHeight;

    var infiniteContainerStyle = {
      height: this.props.containerHeight,
      overflowY: "scroll",
    };
    return (
      <div
        ref="infiniteContainer"
        className="infiniteContainer"
        style={infiniteContainerStyle}
        onScroll={this.onEditorScroll}
        >
          <div ref="topSpacer" className="topSpacer" style={{height: this.topSpacerHeight}}/>
          <div ref="visibleRowsContainer" className="visibleRowsContainer">
            {rowItems}
          </div>
          <div ref="bottomSpacer" className="bottomSpacer" style={{height: this.bottomSpacerHeight}}/>
      </div>
    );
  }
});

module.exports = InfiniteScoller;

Comments

    Ничего не найдено.