javascript - setup - react tutorial
ReactJS: modelado de desplazamiento infinito bidireccional (3)
Esta es una mezcla de una tabla infinita y un escenario de desplazamiento infinito. La mejor abstracción que encontré para esto es la siguiente:
Visión de conjunto
Cree un componente <List>
que tome una matriz de todos los elementos secundarios. Como no los renderizamos, es realmente barato asignarlos y descartarlos. Si las asignaciones de 10k son demasiado grandes, puede pasar una función que toma un rango y devolver los elementos.
<List>
{thousandelements.map(function() { return <Element /> })}
</List>
El componente de su List
realiza un seguimiento de la posición de desplazamiento y solo representa los elementos secundarios que están a la vista. Agrega un gran div vacío al principio para simular los elementos anteriores que no se representan.
Ahora, la parte interesante es que una vez que se representa un componente Element
, se mide su altura y se almacena en su List
. Esto le permite calcular la altura del espaciador y saber cuántos elementos se deben visualizar a la vista.
Imagen
Usted dice que cuando la imagen se está cargando, hace que todo "salte" hacia abajo. La solución para esto es establecer las dimensiones de la imagen en su etiqueta img: <img src="..." width="100" height="58" />
. De esta forma, el navegador no tiene que esperar para descargarlo antes de saber qué tamaño va a mostrar. Esto requiere algo de infraestructura, pero realmente lo vale.
Si no puede conocer el tamaño por adelantado, agregue los oyentes de carga a su imagen y, cuando se cargue, mida la dimensión que se muestra y actualice la altura de la fila almacenada y compense la posición de desplazamiento.
Saltando a un elemento aleatorio
Si necesita saltar a un elemento aleatorio en la lista que va a requerir algún truco con la posición de desplazamiento porque no conoce el tamaño de los elementos intermedios. Lo que sugiero que hagas es promediar las alturas de los elementos que ya has calculado y saltar a la posición de desplazamiento de la última altura conocida + (número de elementos * promedio).
Como esto no es exacto, causará problemas cuando llegue al último puesto bueno conocido. Cuando ocurre un conflicto, simplemente cambie la posición de desplazamiento para solucionarlo. Esto moverá un poco la barra de desplazamiento, pero no debería afectarlo demasiado.
Reaccionar detalles
Desea proporcionar una key para todos los elementos representados para que se mantengan en todos los renders. Hay dos estrategias: (1) tener solo n teclas (0, 1, 2, ... n) donde n es la cantidad máxima de elementos que puede visualizar y usar su módulo de posición n. (2) tienen una clave diferente por elemento. Si todos los elementos comparten una estructura similar, es bueno usar (1) para reutilizar sus nodos DOM. Si no lo hacen, entonces usa (2).
Solo tendría dos piezas del estado Reaccionar: el índice del primer elemento y la cantidad de elementos que se muestran. La posición de desplazamiento actual y la altura de todos los elementos se unirían directamente a this
. Al usar setState
, en realidad está haciendo una reedición, lo que solo debería ocurrir cuando cambia el rango.
Aquí hay un ejemplo de http://jsfiddle.net/vjeux/KbWJ2/9/ de lista infinita usando algunas de las técnicas que describo en esta respuesta. Va a ser algo de trabajo, pero React es definitivamente una buena manera de implementar una lista infinita :)
Nuestra aplicación utiliza desplazamiento infinito para navegar por listas grandes de artículos heterogéneos. Hay algunas arrugas:
- Es común que nuestros usuarios tengan una lista de 10,000 artículos y necesiten desplazarse a través de 3k +.
- Estos son artículos ricos, por lo que solo podemos tener unos cientos en el DOM antes de que el rendimiento del navegador se vuelva inaceptable.
- Los artículos son de diferentes alturas.
- Los elementos pueden contener imágenes y permitimos que el usuario salte a una fecha específica. Esto es complicado porque el usuario puede saltar a un punto de la lista donde tenemos que cargar imágenes sobre la ventana gráfica, lo que empujaría el contenido hacia abajo cuando se carguen. Si no se maneja eso, significa que el usuario puede saltar a una fecha, pero luego cambiar a una fecha anterior.
Soluciones conocidas e incompletas:
https://github.com/guillaumervls/react-infinite-scroll : este es solo un simple componente "cargar más cuando llegamos al final". No elimina ningún DOM, por lo que morirá en miles de elementos.
http://blog.vjeux.com/2013/javascript/scroll-position-with-react.html - Muestra cómo almacenar y restaurar la posición de desplazamiento al insertar en la parte superior o insertar en la parte inferior, pero no ambos juntos.
No estoy buscando el código para una solución completa (aunque sería genial). En cambio, estoy buscando el "modo Reaccionar" para modelar esta situación. ¿Está la posición de desplazamiento en estado o no? ¿Qué estado debo seguir para mantener mi posición en la lista? ¿Qué estado debo mantener para activar un nuevo renderizado cuando me desplazo cerca de la parte inferior o superior de lo que se renderiza?
Me enfrentaba a un desafío similar para modelar el desplazamiento infinito de una sola dirección con alturas heterogéneas de elementos y, por lo tanto, hice un paquete npm de mi solución:
https://www.npmjs.com/package/react-variable-height-infinite-scroller
y una demostración: http://tnrich.github.io/react-variable-height-infinite-scroller/
Puede consultar el código fuente de la lógica, pero básicamente seguí la receta @Vjeux descrita en la respuesta anterior. Todavía no he abordado el salto a un elemento en particular, pero espero implementarlo pronto.
Aquí está lo esencial de cómo se ve el código actualmente:
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;
eche un vistazo a http://adazzle.github.io/react-data-grid/index.html# Esto se ve como una cuadrícula de datos potente y de rendimiento con características similares a Excel y carga lenta / procesamiento optimizado (para millones de filas) con ricas funciones de edición (licencia MIT). Todavía no lo hemos probado en nuestro proyecto, pero lo haremos muy pronto.
Un gran recurso para buscar cosas como estas también es http://react.rocks/ En este caso, una búsqueda de etiquetas es útil: http://react.rocks/tag/InfiniteScroll