Вопрос по javascript, infinite-scroll, reactjs, html – ReactJS: моделирование двунаправленной бесконечной прокрутки

112

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

Это'Для наших пользователей характерно иметь список из 10 000 наименований, и им необходимо пролистать 3k +.Это богатые элементы, поэтому у нас может быть всего несколько сотен в DOM, прежде чем производительность браузера станет неприемлемой.Предметы разной высоты.Элементы могут содержать изображения, и мы позволяем пользователю перейти к определенной дате. Это сложно, потому что пользователь может перейти к точке в списке, где нам нужно загрузить изображения над областью просмотра, что будет толкать содержимое вниз при загрузке. Неспособность обработать это означает, что пользователь может перейти к дате, но затем будет перенесен на более раннюю дату.

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

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

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

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

Ваш Ответ

3   ответа
113

абстракция, которую я нашел для этого, следующая:

обзор

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

<list>
  {thousandelements.map(function() { return <element> })}
</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).

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

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

Я так думаюvisibleStart а такжеvisibleEnd говорят сами за себя. Они представляют индексs записей, которые фактически видны в прокручиваемом контейнере.displayStart а такжеdisplayEnd Похоже, это диапазон записей для монтирования в дом.displayStart будет в основном меньшеvisibleStart были возможны в случае, если пользователь прокручивает вверх.displayEnd будет +visibleStartrecordsPerBody * произвольное число, если пользователь прокручивает страницу вниз. По сути, то, что они допускают, это немного буфера. Mark Murphy
Я реализовал это с изюминкой, и столкнулся с проблемой: для меня, записи, которые яm рендеринг - это несколько сложный DOM, и из-за количества их нецелесообразно загружать их все в браузер, поэтому я время от времени делаю асинхронные выборки. По какой-то причине, когда я прокручиваюсь и прыжки положения очень далеко (скажем, я выхожу за пределы экрана и обратно), ListBody не 'перерисовать, даже если состояние меняется. Есть идеи, почему это может быть? Отличный пример в противном случае! SleepyProgrammer
Это потрясающая техника. Спасибо! Я получил это работает на одном из моих компонентов. Тем не менее, у меня есть еще один компонент, который яЯ хотел бы применить это к, но строки нене иметь постоянной высоты. Я'я работаю над расширением вашего примера, чтобы вычислить displayEnd / visibleEnd для учета различной высоты ... разве у вас нет лучшей идеи? manalang
@ThomasModeneis привет, вы можете уточнить вычисления, выполненные в строках 151 и 152, displayStart и displayEnd shortCircuit
1

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

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

1

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

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

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

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

Вот's тонкость того, как код в настоящее время выглядит:

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 + adjustInfiniteCo,ntainerByThisAmount;
    } 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 (
      
          
          
            {rowItems}
          
          
      
    );
  }
});

module.exports = InfiniteScoller;

Похожие вопросы