ReactJS: Modélisation Bidirectionnelle De Défilement Infini

notre application utilise le défilement infini pour naviguer de grandes listes d'articles hétérogènes. Il y a quelques rides:

  • " il est fréquent que nos utilisateurs aient une liste de 10 000 articles et aient besoin de faire défiler leur liste à travers 3k+.
  • ce sont des articles riches, donc nous ne pouvons avoir que quelques centaines dans le DOM avant que les performances du navigateur devient inacceptable.
  • les objets sont de différentes hauteurs.
  • les articles peuvent contient des images et nous permettons à l'utilisateur de passer à une date précise. C'est délicat parce que l'utilisateur peut sauter à un point dans la liste où nous devons charger des images au-dessus du viewport, ce qui pousserait le contenu vers le bas quand ils chargent. À défaut de gérer cela signifie que l'utilisateur peut accéder à une date, mais alors décalée à une date antérieure.

solutions connues, incomplètes:

Je ne cherche pas le code pour une solution complète (bien que ce serait génial. Au lieu de cela, je cherche la "façon de réagir" pour modéliser cette situation. La position de scroll est-elle ou non? Quel état dois-je suivre pour conserver ma position sur la liste? Quel état dois-je conserver pour déclencher un nouveau rendu lorsque je fais défiler vers le bas ou le haut de ce qui est rendu?

110
demandé sur noah 2014-01-01 20:07:07
la source

3 ответов

c'est un mélange d'une table infinie et d'un scénario de défilement infini. La meilleure abstraction que j'ai trouvé pour cela est la suivante:

vue d'ensemble

faire un <List> composant qui prend un tableau de tous enfants. Puisque nous ne les rendons pas, il est vraiment bon marché de simplement les allouer et les jeter. Si 10k allocations est trop grand, vous pouvez passer une fonction qui prend une gamme et retourner les éléments.

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

votre composant List garde une trace de la position de défilement et ne rend que les enfants qui sont en vue. Il ajoute une grande div vide au début de faux éléments précédents qui ne sont pas rendus.

maintenant, la partie intéressante est qu'une fois qu'un composant Element est rendu, vous mesurez sa hauteur et le stockez dans votre List . Cela vous permet de calculer la hauteur de l'entretoise et savoir combien d'éléments doivent être affichés en vue.

Image

vous dites que lorsque l'image est chargée, tout" saute " vers le bas. La solution pour cela est de définir les dimensions de l'image dans votre étiquette img: <img src="..." width="100" height="58" /> . De cette façon, le navigateur n'a pas à attendre pour le télécharger avant de savoir de quelle taille il va être affichée. Cela nécessite certaines infrastructures, mais cela en vaut vraiment la peine.

si vous ne pouvez pas connaître la taille à l'avance, ajoutez les écouteurs onload à votre image et quand elle est chargée, mesurez sa dimension affichée et mettez à jour la hauteur de la ligne stockée et compensez la position de défilement.

saut à un élément aléatoire

si vous avez besoin de sauter à un élément aléatoire dans la liste qui va exiger une certaine ruse avec la position de défilement parce que vous ne savez pas la taille des éléments dans entre. Ce que je vous suggère de faire est de faire la moyenne des hauteurs de l'élément que vous avez déjà calculé et de sauter à la position de défilement de la dernière hauteur connue + (nombre d'éléments * moyenne).

comme ce n'est pas exact il va causer des problèmes quand vous atteignez la dernière bonne position connue. Quand un conflit se produit, il suffit de changer la position de défilement pour le corriger. Cela va déplacer la barre de défilement un peu mais ne devrait pas trop affecter lui/elle.

Réagir Détails

vous voulez fournir une clé à tous les éléments rendus afin qu'ils soient maintenus à travers les rendus. Il y a deux stratégies: (1) n'ont que n clés (0, 1, 2, ... n) où n est le nombre maximum d'éléments que vous pouvez afficher et utiliser leur position modulo N. (2) avoir une clé différente par élément. Si tous les éléments partagent une structure similaire, il est bon d'utiliser (1) pour réutiliser leurs noeuds DOM. Si ils ne le font pas puis utilisez (2).

je n'aurais que deux morceaux de Réagir état: l'indice du premier élément et le nombre d'éléments affichés. La position actuelle du rouleau et la hauteur de tous les éléments seraient directement rattachées à this . Lorsque vous utilisez setState , vous faites en fait une redirection qui ne devrait se produire que lorsque la plage change.

voici un exemple http://jsfiddle.net/vjeux/KbWJ2/9 / de liste infinie en utilisant certaines des techniques que je décris dans cette réponse. Il va y avoir du travail, mais React est définitivement une bonne façon de mettre en œuvre une liste infinie:)

113
répondu Vjeux 2016-02-01 13:46:47
la source

regardez http://adazzle.github.io/react-data-grid/index.html # Cela ressemble à un datagrid puissant et performant avec des fonctionnalités de type Excel et un chargement paresseux/un rendu optimisé (pour des millions de lignes) avec de riches fonctionnalités d'édition (licence MIT). Pas encore essayé dans notre projet, mais le faire très bientôt.

une grande ressource pour chercher des choses comme celles-ci est aussi http://react.roches / Dans ce cas, une balise la recherche est utile: http://react.rocks / tag / InfiniteScroll

1
répondu Gregor 2015-08-13 16:22:46
la source

j'ai été confronté à un défi similaire pour la modélisation simple-direction infinie défilement avec des hauteurs d'article hétérogène et donc fait un paquet npm de ma solution:

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

et une démo: http://tnrich.github.io/react-variable-height-infinite-scroller /

vous pouvez consulter le code source logique, mais j'ai essentiellement suivi la recette @Vjeux décrit dans la réponse ci-dessus. Je n'ai pas encore abordé le saut à un élément particulier, mais j'espère l'implémenter bientôt.

voici à quoi ressemble actuellement le code:

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;
1
répondu majorBummer 2015-08-22 12:16:41
la source

Autres questions sur