Ich bin dabei, eine filterbare Liste mit React zu implementieren. Die Struktur der Liste ist wie in der Abbildung unten dargestellt.
PRÄMISSE
Hier ist eine Beschreibung, wie es funktionieren soll:
- Der Status befindet sich in der Komponente der höchsten Ebene, der
Search
Komponente. - Der Zustand wird wie folgt beschrieben:
{ sichtbar: boolean, Dateien: Array, gefiltert: Array, Abfrage: Zeichenfolge, currentSelectedIndex: Ganzzahl }}
files
ist ein potenziell sehr großes Array mit Dateipfaden (10000 Einträge sind eine plausible Zahl).filtered
ist das gefilterte Array, nachdem der Benutzer mindestens 2 Zeichen eingegeben hat. Ich weiß, dass es sich um abgeleitete Daten handelt, und als solches könnte man argumentieren, sie im Staat zu speichern, aber es wird für benötigtcurrentlySelectedIndex
Dies ist der Index des aktuell ausgewählten Elements aus der gefilterten Liste.Der Benutzer gibt mehr als 2 Buchstaben in die
Input
Komponente ein, das Array wird gefiltert und für jeden Eintrag im gefilterten Array wird eineResult
Komponente gerendertJede
Result
Komponente zeigt den vollständigen Pfad an, der teilweise mit der Abfrage übereinstimmt, und der teilweise übereinstimmende Teil des Pfads wird hervorgehoben. Zum Beispiel wäre das DOM einer Ergebniskomponente, wenn der Benutzer 'le' eingegeben hätte, ungefähr so:<li>this/is/a/fi<strong>le</strong>/path</li>
- Wenn der Benutzer die Aufwärts- oder Abwärtstaste drückt, während die
Input
Komponente fokussiert ist, werden diecurrentlySelectedIndex
Änderungen basierend auf demfiltered
Array vorgenommen. Dadurch wird dieResult
Komponente, die dem Index entspricht, als ausgewählt markiert, was zu einem erneuten Rendern führt
PROBLEM
Anfangs habe ich dies mit einem ausreichend kleinen Array files
unter Verwendung der Entwicklungsversion von React getestet , und alles hat gut funktioniert.
Das Problem trat auf, als ich mich mit einem files
Array mit bis zu 10000 Einträgen befassen musste . Wenn Sie 2 Buchstaben in die Eingabe eingeben, wird eine große Liste erstellt, und wenn ich die Auf- und Ab-Tasten zum Navigieren drücke, ist dies sehr verzögert.
Zuerst hatte ich keine definierte Komponente für die Result
Elemente und ich erstellte lediglich die Liste im laufenden Betrieb bei jedem Rendering der Search
Komponente als solche:
results = this.state.filtered.map(function(file, index) {
var start, end, matchIndex, match = this.state.query;
matchIndex = file.indexOf(match);
start = file.slice(0, matchIndex);
end = file.slice(matchIndex + match.length);
return (
<li onClick={this.handleListClick}
data-path={file}
className={(index === this.state.currentlySelected) ? "valid selected" : "valid"}
key={file} >
{start}
<span className="marked">{match}</span>
{end}
</li>
);
}.bind(this));
Wie Sie sehen können, würde jedes Mal, wenn die currentlySelectedIndex
Änderung vorgenommen wird, ein erneutes Rendern verursacht und die Liste jedes Mal neu erstellt. Ich dachte, da ich key
für jedes li
Element einen Wert festgelegt hatte, würde React vermeiden, jedes andere li
Element neu zu rendern , das seine className
Änderung nicht hatte, aber anscheinend war es nicht so.
Am Ende habe ich eine Klasse für die Result
Elemente definiert, in der explizit geprüft wird, ob jedes Result
Element neu gerendert werden soll, basierend darauf, ob es zuvor ausgewählt wurde und basierend auf der aktuellen Benutzereingabe:
var ResultItem = React.createClass({
shouldComponentUpdate : function(nextProps) {
if (nextProps.match !== this.props.match) {
return true;
} else {
return (nextProps.selected !== this.props.selected);
}
},
render : function() {
return (
<li onClick={this.props.handleListClick}
data-path={this.props.file}
className={
(this.props.selected) ? "valid selected" : "valid"
}
key={this.props.file} >
{this.props.children}
</li>
);
}
});
Und die Liste wird jetzt als solche erstellt:
results = this.state.filtered.map(function(file, index) {
var start, end, matchIndex, match = this.state.query, selected;
matchIndex = file.indexOf(match);
start = file.slice(0, matchIndex);
end = file.slice(matchIndex + match.length);
selected = (index === this.state.currentlySelected) ? true : false
return (
<ResultItem handleClick={this.handleListClick}
data-path={file}
selected={selected}
key={file}
match={match} >
{start}
<span className="marked">{match}</span>
{end}
</ResultItem>
);
}.bind(this));
}
Dies hat die Leistung etwas verbessert , ist aber immer noch nicht gut genug. Als ich die Produktionsversion von React testete, funktionierten die Dinge butterweich und ohne Verzögerung.
ENDEFFEKT
Ist eine solche merkliche Diskrepanz zwischen Entwicklungs- und Produktionsversionen von React normal?
Verstehe / mache ich etwas falsch, wenn ich darüber nachdenke, wie React die Liste verwaltet?
UPDATE 14-11-2016
Ich habe diese Präsentation von Michael Jackson gefunden, in der er ein Problem behandelt, das diesem sehr ähnlich ist: https://youtu.be/7S8v8jfLb1Q?t=26m2s
Die Lösung ist der in der Antwort von AskarovBeknar unten vorgeschlagenen sehr ähnlich
UPDATE 14-4-2018
Da dies anscheinend eine beliebte Frage ist und sich die Dinge seit der ursprünglichen Frage weiterentwickelt haben, empfehle ich Ihnen, das oben verlinkte Video anzusehen, um sich ein Bild von einem virtuellen Layout zu machen. Ich empfehle Ihnen jedoch auch, React Virtualized zu verwenden Bibliothek, wenn Sie das Rad nicht neu erfinden möchten.
quelle
Antworten:
Wie bei vielen anderen Antworten auf diese Frage liegt das Hauptproblem in der Tatsache, dass das Rendern so vieler Elemente im DOM beim Filtern und Behandeln von Schlüsselereignissen langsam sein wird.
Sie machen in Bezug auf React, das das Problem verursacht, nichts von Natur aus falsch, aber wie viele der leistungsbezogenen Probleme kann auch die Benutzeroberfläche einen großen Prozentsatz der Schuld tragen.
Wenn Ihre Benutzeroberfläche nicht auf Effizienz ausgelegt ist, leiden auch Tools wie React, die auf Leistung ausgelegt sind.
Das Filtern der Ergebnismenge ist ein guter Anfang, wie von @Koen erwähnt
Ich habe ein bisschen mit der Idee herumgespielt und eine Beispiel-App erstellt, die zeigt, wie ich anfangen kann, diese Art von Problem anzugehen.
Dies ist keineswegs
production ready
Code, aber er veranschaulicht das Konzept angemessen und kann modifiziert werden, um robuster zu sein. Schauen Sie sich den Code an - ich hoffe, es gibt Ihnen zumindest einige Ideen ...;)React-Large-List-Beispiel
quelle
127.0.0.1 * http://localhost:3001
?Meine Erfahrung mit einem sehr ähnlichen Problem ist, dass die Reaktion wirklich leidet, wenn mehr als 100-200 Komponenten gleichzeitig im DOM vorhanden sind. Selbst wenn Sie sehr vorsichtig sind (indem Sie alle Schlüssel einrichten und / oder eine
shouldComponentUpdate
Methode implementieren ), um nur eine oder zwei Komponenten bei einem erneuten Rendern zu ändern, befinden Sie sich immer noch in einer Welt voller Verletzungen.Der langsame Teil der Reaktion im Moment ist, wenn der Unterschied zwischen dem virtuellen DOM und dem realen DOM verglichen wird. Wenn Sie Tausende von Komponenten haben, aber nur ein paar aktualisieren, spielt es keine Rolle, dass React immer noch einen massiven Unterschied zwischen den DOMs hat.
Wenn ich jetzt Seiten schreibe, versuche ich, sie so zu gestalten, dass die Anzahl der Komponenten minimiert wird. Eine Möglichkeit, dies beim Rendern großer Listen von Komponenten zu tun, besteht darin, ... nun ... keine großen Listen von Komponenten zu rendern.
Was ich damit meine ist: Rendern Sie nur die Komponenten, die Sie derzeit sehen können, rendern Sie mehr, während Sie nach unten scrollen. Ihr Benutzer wird wahrscheinlich nicht durch Tausende von Komponenten nach unten scrollen. Ich hoffe.
Eine großartige Bibliothek dafür ist:
https://www.npmjs.com/package/react-infinite-scroll
Mit einer tollen Anleitung hier:
http://www.reactexamples.com/react-infinite-scroll/
Ich befürchte, es werden keine Komponenten entfernt, die sich oben auf der Seite befinden. Wenn Sie also lange genug scrollen, treten wieder Leistungsprobleme auf.
Ich weiß, dass es keine gute Praxis ist, einen Link als Antwort bereitzustellen, aber die Beispiele, die sie bereitstellen, werden erklären, wie diese Bibliothek viel besser verwendet wird, als ich hier kann. Hoffentlich habe ich erklärt, warum große Listen schlecht sind, aber auch eine Lösung.
quelle
Erstens ist der Unterschied zwischen der Entwicklungs- und der Produktionsversion von React sehr groß, da in der Produktion viele umgangene Hygienekontrollen durchgeführt werden (z. B. Überprüfung der Requisitentypen).
Dann sollten Sie die Verwendung von Redux überdenken, da dies hier äußerst hilfreich für das ist, was Sie benötigen (oder für jede Art von Flussmittelimplementierung). Schauen Sie sich unbedingt diese Präsentation an: Big List High Performance React & Redux .
Bevor Sie jedoch in Redux eintauchen, müssen Sie einige Anpassungen an Ihrem React-Code vornehmen, indem Sie Ihre Komponenten in kleinere Komponenten aufteilen, da
shouldComponentUpdate
das Rendern von Kindern vollständig umgangen wird. Dies ist also ein enormer Gewinn .Wenn Sie detailliertere Komponenten haben, können Sie den Status mit Redux und React-Redux behandeln, um den Datenfluss besser zu organisieren.
Ich hatte kürzlich ein ähnliches Problem, als ich tausend Zeilen rendern und jede Zeile durch Bearbeiten ihres Inhalts ändern musste. Diese Mini-App zeigt eine Liste von Konzerten mit potenziellen Duplikaten an. Ich muss für jedes potenzielle Duplikat auswählen, ob ich das potenzielle Duplikat als Originalkonzert (kein Duplikat) markieren möchte, indem ich das Kontrollkästchen aktiviere und gegebenenfalls das bearbeite Name des Konzerts. Wenn ich für ein bestimmtes potenzielles doppeltes Element nichts unternehme, wird es als doppelt betrachtet und gelöscht.
So sieht es aus:
Grundsätzlich gibt es 4 Netzkomponenten (hier gibt es nur eine Zeile, aber nur zum Beispiel):
Hier ist der vollständige Code (funktionierender CodePen: Riesige Liste mit React & Redux ) unter Verwendung von Redux , React -Redux , unveränderlich , Neuauswahl und Neuzusammenstellung :
const initialState = Immutable.fromJS({ /* See codepen, this is a HUGE list */ }) const types = { CONCERTS_DEDUP_NAME_CHANGED: 'diggger/concertsDeduplication/CONCERTS_DEDUP_NAME_CHANGED', CONCERTS_DEDUP_CONCERT_TOGGLED: 'diggger/concertsDeduplication/CONCERTS_DEDUP_CONCERT_TOGGLED', }; const changeName = (pk, name) => ({ type: types.CONCERTS_DEDUP_NAME_CHANGED, pk, name }); const toggleConcert = (pk, toggled) => ({ type: types.CONCERTS_DEDUP_CONCERT_TOGGLED, pk, toggled }); const reducer = (state = initialState, action = {}) => { switch (action.type) { case types.CONCERTS_DEDUP_NAME_CHANGED: return state .updateIn(['names', String(action.pk)], () => action.name) .set('_state', 'not_saved'); case types.CONCERTS_DEDUP_CONCERT_TOGGLED: return state .updateIn(['concerts', String(action.pk)], () => action.toggled) .set('_state', 'not_saved'); default: return state; } }; /* configureStore */ const store = Redux.createStore( reducer, initialState ); /* SELECTORS */ const getDuplicatesGroups = (state) => state.get('duplicatesGroups'); const getDuplicateGroup = (state, name) => state.getIn(['duplicatesGroups', name]); const getConcerts = (state) => state.get('concerts'); const getNames = (state) => state.get('names'); const getConcertName = (state, pk) => getNames(state).get(String(pk)); const isConcertOriginal = (state, pk) => getConcerts(state).get(String(pk)); const getGroupNames = reselect.createSelector( getDuplicatesGroups, (duplicates) => duplicates.flip().toList() ); const makeGetConcertName = () => reselect.createSelector( getConcertName, (name) => name ); const makeIsConcertOriginal = () => reselect.createSelector( isConcertOriginal, (original) => original ); const makeGetDuplicateGroup = () => reselect.createSelector( getDuplicateGroup, (duplicates) => duplicates ); /* COMPONENTS */ const DuplicatessTableRow = Recompose.onlyUpdateForKeys(['name'])(({ name }) => { return ( <tr> <td>{name}</td> <DuplicatesRowColumn name={name}/> </tr> ) }); const PureToggle = Recompose.onlyUpdateForKeys(['toggled'])(({ toggled, ...otherProps }) => ( <input type="checkbox" defaultChecked={toggled} {...otherProps}/> )); /* CONTAINERS */ let DuplicatesTable = ({ groups }) => { return ( <div> <table className="pure-table pure-table-bordered"> <thead> <tr> <th>{'Concert'}</th> <th>{'Duplicates'}</th> </tr> </thead> <tbody> {groups.map(name => ( <DuplicatesTableRow key={name} name={name} /> ))} </tbody> </table> </div> ) }; DuplicatesTable.propTypes = { groups: React.PropTypes.instanceOf(Immutable.List), }; DuplicatesTable = ReactRedux.connect( (state) => ({ groups: getGroupNames(state), }) )(DuplicatesTable); let DuplicatesRowColumn = ({ duplicates }) => ( <td> <ul> {duplicates.map(d => ( <DuplicateItem key={d} pk={d}/> ))} </ul> </td> ); DuplicatessRowColumn.propTypes = { duplicates: React.PropTypes.arrayOf( React.PropTypes.string ) }; const makeMapStateToProps1 = (_, { name }) => { const getDuplicateGroup = makeGetDuplicateGroup(); return (state) => ({ duplicates: getDuplicateGroup(state, name) }); }; DuplicatesRowColumn = ReactRedux.connect(makeMapStateToProps1)(DuplicatesRowColumn); let DuplicateItem = ({ pk, name, toggled, onToggle, onNameChange }) => { return ( <li> <table> <tbody> <tr> <td>{ toggled ? <input type="text" value={name} onChange={(e) => onNameChange(pk, e.target.value)}/> : name }</td> <td> <PureToggle toggled={toggled} onChange={(e) => onToggle(pk, e.target.checked)}/> </td> </tr> </tbody> </table> </li> ) } const makeMapStateToProps2 = (_, { pk }) => { const getConcertName = makeGetConcertName(); const isConcertOriginal = makeIsConcertOriginal(); return (state) => ({ name: getConcertName(state, pk), toggled: isConcertOriginal(state, pk) }); }; DuplicateItem = ReactRedux.connect( makeMapStateToProps2, (dispatch) => ({ onNameChange(pk, name) { dispatch(changeName(pk, name)); }, onToggle(pk, toggled) { dispatch(toggleConcert(pk, toggled)); } }) )(DuplicateItem); const App = () => ( <div style={{ maxWidth: '1200px', margin: 'auto' }}> <DuplicatesTable /> </div> ) ReactDOM.render( <ReactRedux.Provider store={store}> <App/> </ReactRedux.Provider>, document.getElementById('app') );
Lehren aus dieser Mini-App bei der Arbeit mit riesigen Datenmengen
connect
Komponente für eine Komponente, die den Daten am nächsten kommt, die sie benötigen, um zu vermeiden, dass die Komponente nur Requisiten weitergibt, die sie nicht verwendenownProps
ist erforderlich, um unnötiges erneutes Rendern zu vermeidenquelle
Reagieren Sie in der Entwicklungsversion auf Proptypen jeder Komponente, um den Entwicklungsprozess zu vereinfachen, während in der Produktion darauf verzichtet wird.
Das Filtern der Liste von Zeichenfolgen ist für jede Eingabe sehr teuer. Dies kann zu Leistungsproblemen führen, da JavaScript nur einen Thread enthält. Die Lösung könnte darin bestehen, die Entprellungsmethode zu verwenden, um die Ausführung Ihrer Filterfunktion zu verzögern, bis die Verzögerung abgelaufen ist.
Ein weiteres Problem könnte die riesige Liste selbst sein. Sie können ein virtuelles Layout erstellen und erstellte Elemente wiederverwenden, indem Sie nur Daten ersetzen. Grundsätzlich erstellen Sie eine scrollbare Containerkomponente mit fester Höhe, in die Sie den Listencontainer einfügen. Die Höhe des Listencontainers sollte abhängig von der Länge der sichtbaren Liste manuell festgelegt werden (itemHeight * numberOfItems), damit eine Bildlaufleiste funktioniert. Erstellen Sie dann einige Elementkomponenten, damit sie die Höhe des scrollbaren Containers ausfüllen, und fügen Sie möglicherweise ein oder zwei zusätzliche Nachahmungseffekte hinzu. Machen Sie sie zur absoluten Position und verschieben Sie beim Scrollen einfach ihre Position, damit sie eine fortlaufende Liste imitiert (ich denke, Sie werden herausfinden, wie Sie sie implementieren können :)
Eine weitere Sache ist, dass das Schreiben in DOM auch eine teure Operation ist, insbesondere wenn Sie es falsch machen. Sie können die Leinwand zum Anzeigen von Listen verwenden und ein reibungsloses Bildlauferlebnis erzielen. Checkout-React-Canvas-Komponenten. Ich habe gehört, dass sie bereits einige Arbeiten an Listen durchgeführt haben.
quelle
React in development
? und warum nach Prototypen jeder Komponente suchen?Schauen Sie sich React Virtualized Select an. Es wurde entwickelt, um dieses Problem zu beheben, und bietet meiner Erfahrung nach eine beeindruckende Leistung. Aus der Beschreibung:
https://github.com/bvaughn/react-virtualized-select
quelle
Wie ich in meinem Kommentar erwähnt habe , bezweifle ich, dass Benutzer alle diese 10000 Ergebnisse gleichzeitig im Browser benötigen.
Was ist, wenn Sie durch die Ergebnisse blättern und immer nur eine Liste mit 10 Ergebnissen anzeigen?
Ich habe mit dieser Technik ein Beispiel erstellt , ohne eine andere Bibliothek wie Redux zu verwenden. Derzeit nur mit Tastaturnavigation, kann aber problemlos erweitert werden, um auch beim Scrollen zu arbeiten.
Das Beispiel besteht aus 3 Komponenten, der Containeranwendung, einer Suchkomponente und einer Listenkomponente. Fast die gesamte Logik wurde in die Containerkomponente verschoben.
Das Wesentliche liegt darin,
start
dasselected
Ergebnis zu verfolgen und die Interaktion mit der Tastatur zu verschieben.nextResult: function() { var selected = this.state.selected + 1 var start = this.state.start if(selected >= start + this.props.limit) { ++start } if(selected + start < this.state.results.length) { this.setState({selected: selected, start: start}) } }, prevResult: function() { var selected = this.state.selected - 1 var start = this.state.start if(selected < start) { --start } if(selected + start >= 0) { this.setState({selected: selected, start: start}) } },
Während Sie einfach alle Dateien durch einen Filter leiten:
updateResults: function() { var results = this.props.files.filter(function(file){ return file.file.indexOf(this.state.query) > -1 }, this) this.setState({ results: results }); },
Und die Ergebnisse basierend auf
start
undlimit
in derrender
Methode aufteilen:render: function() { var files = this.state.results.slice(this.state.start, this.state.start + this.props.limit) return ( <div> <Search onSearch={this.onSearch} onKeyDown={this.onKeyDown} /> <List files={files} selected={this.state.selected - this.state.start} /> </div> ) }
Geige mit einem voll funktionsfähigen Beispiel: https://jsfiddle.net/koenpunt/hm1xnpqk/
quelle
Versuchen Sie es mit einem Filter, bevor Sie ihn in die React-Komponente laden. Zeigen Sie nur eine angemessene Anzahl von Elementen in der Komponente an und laden Sie bei Bedarf mehr. Niemand kann so viele Elemente gleichzeitig anzeigen.
Ich glaube nicht, dass Sie es sind, aber verwenden Sie keine Indizes als Schlüssel .
Um herauszufinden, warum die Entwicklungs- und Produktionsversionen unterschiedlich sind, können Sie
profiling
Ihren Code ausprobieren .Laden Sie Ihre Seite, starten Sie die Aufnahme, führen Sie eine Änderung durch, beenden Sie die Aufnahme und überprüfen Sie die Timings. Sehen Sie hier , um Anweisungen für Leistungsprofil in Chrome .
quelle
Für alle, die mit diesem Problem zu kämpfen haben, habe ich eine Komponente geschrieben
react-big-list
, die Listen mit bis zu 1 Million Datensätzen verarbeitet.Darüber hinaus bietet es einige ausgefallene Zusatzfunktionen wie:
Wir verwenden es in der Produktion in einigen Apps und es funktioniert großartig.
quelle
React hat eine empfohlene
react-window
Bibliothek: https://www.npmjs.com/package/react-windowEs ist besser als
react-vitualized
. Du kannst es versuchenquelle