React implementiert eine hochadaptive virtuelle Liste

React implementiert eine hochadaptive virtuelle Liste

Kürzlich stieß ich bei der Entwicklung und Iteration einer bestimmten Plattform auf eine Situation, in der eine extrem lange Liste in einem Antd-Modal verschachtelt war und langsam geladen wurde und es zu Störungen kam. Daher habe ich beschlossen, zur Optimierung des Gesamterlebnisses eine virtuelle Scroll-Liste von Grund auf neu zu implementieren.

Vor der Transformation:

Wir können sehen, dass es vor der Transformation beim Öffnen des Bearbeitungsfensters Modal zu einem kurzen Einfrieren kommt und dass es nach dem Klicken auf „Abbrechen“, um es zu schließen, nicht sofort reagiert, sondern sich nach einer kurzen Verzögerung schließt.

Nach der Transformation:

Nach Abschluss der Transformation können wir beobachten, dass das Öffnen des gesamten Modals viel reibungsloser geworden ist als zuvor und sofort auf das Klickereignis des Benutzers reagieren kann, um das Modal aufzurufen/zu schließen.

Leistungsvergleichsdemo: codesandbox.io/s/av-list-…

0x0-Grundlagen

Was ist also virtuelles Scrollen/Auflisten?

Eine virtuelle Liste bedeutet, dass wir, wenn wir Tausende von Daten anzuzeigen haben, das „Fenster“ des Benutzers (was gleichzeitig sichtbar ist) jedoch nicht groß ist, eine clevere Methode verwenden können, um nur die maximale Anzahl sichtbarer Elemente + „BufferSize“-Elemente darzustellen und den Inhalt jedes Elements dynamisch zu aktualisieren, wenn der Benutzer scrollt. Auf diese Weise erzielen wir denselben Effekt wie beim Scrollen durch eine lange Liste, jedoch mit sehr wenigen Ressourcen.

(Aus dem obigen Bild können wir erkennen, dass die tatsächlichen Elemente/Inhalte, die Benutzer jedes Mal sehen können, nur die Elemente 4 bis 13 sind, also 9 Elemente.)

0x1 Implementierung einer virtuellen Liste mit „fester Höhe“

Zuerst müssen wir einige Variablen/Namen definieren.

  • Aus der obigen Abbildung können wir erkennen, dass das Startelement des tatsächlich sichtbaren Bereichs des Benutzers Item-4 ist, sodass der entsprechende Index im Datenarray unser StartIndex ist.
  • Ebenso sollte der Array-Index, der Item-13 entspricht, unser endIndex sein
  • Damit die Elemente 1, 2 und 3 durch die Wischbewegung des Benutzers nach oben ausgeblendet werden, nennen wir es „startOffset(scrollTop)“.

Da wir den Inhalt nur im sichtbaren Bereich rendern, müssen wir die Höhe der ursprünglichen Liste beibehalten, um das Verhalten des gesamten Containers ähnlich einer langen Liste (Scrollen) beizubehalten. Daher entwerfen wir die HTML-Struktur wie folgt

<!--ver 1.0 -->
<div Klassenname="vListContainer">
  <div Klassenname="Phantominhalt">
    ...
    <!-- Artikel-1 -->
    <!-- Artikel-2 -->
    <!-- Artikel-3 -->
    ....
  </div>
</div>

In:

  • vListContainer ist der Container des sichtbaren Bereichs und verfügt über die Eigenschaft overflow-y: auto.
  • Jedes Datenelement im Phantom sollte eine absolute Position haben
  • PhantomContent ist unser „Phantom“-Teil und sein Hauptzweck besteht darin, die Inhaltshöhe der echten Liste wiederherzustellen, um das normale Bildlaufverhalten einer langen Liste zu simulieren.

Als Nächstes binden wir eine OnScroll-Antwortfunktion an vListContainer und berechnen unseren Startindex und Endindex in der Funktion entsprechend der ScrollTop-Eigenschaft des nativen Scroll-Ereignisses.

  • Bevor wir mit der Berechnung beginnen, müssen wir einige Werte definieren:

Wir brauchen eine feste Listenelementhöhe: rowHeight
Wir müssen wissen, wie viele Elemente sich in der aktuellen Liste befinden: insgesamt
Wir müssen die Höhe des sichtbaren Bereichs des aktuellen Benutzers kennen: Höhe

  • Mit den obigen Daten können wir die folgenden Daten berechnen:

Gesamthöhe der Liste: Phantomhöhe = Gesamthöhe * Zeilenhöhe
Anzahl der im sichtbaren Bereich angezeigten Elemente: limit = Math.ceil(height/rowHeight)

Daher können wir im onScroll-Callback die folgende Berechnung durchführen:

beimScrollen(evt: beliebig) {
  // Bestimmen Sie, ob es sich um ein Scroll-Ereignis handelt, auf das wir reagieren müssen, wenn (evt.target === this.scrollingContainer.current) {
    const { scrollTop } = evt.target;
    const { StartIndex, Gesamt, Zeilenhöhe, Limit } = dies;

    // Den aktuellen StartIndex berechnen
    const currentStartIndex = Math.floor(scrollTop / rowHeight);

    // Wenn currentStartIndex sich von startIndex unterscheidet (wir müssen die Daten aktualisieren)
    wenn (aktuellerStartIndex !== startIndex ) {
      dieser.startIndex = aktuellerStartIndex;
      this.endIndex = Math.min(currentStartIndexx + limit, total - 1);
      this.setState({ scrollTop });
    }
  }
}

Sobald wir den Startindex und den Endindex haben, können wir die entsprechenden Daten rendern:

renderDisplayContent = () => {
  const { Zeilenhöhe, Startindex, Endindex } = dies;
  const Inhalt = [];
  
  // Beachten Sie, dass wir hier <= verwenden, um x+1 Elemente zu rendern und so das Scrollen kontinuierlich zu machen (immer im Einklang mit der Beurteilung rendern und x+2 rendern)
  für (lass i = Startindex; i <= Endindex; ++i) {
    // rowRenderer ist eine benutzerdefinierte Methode zum Rendern von Listenelementen, die einen Index i und // den Stil erhalten muss, der der aktuellen Position entspricht
    Inhalt.push(
      rowRenderer({
        Index: ich, 
        Stil: {
          Breite: '100%',
          Höhe: Zeilenhöhe + 'px',
          Position: "absolut",
          links: 0,
          rechts: 0,
          oben: i * Zeilenhöhe,
          Rahmen unten: "1px durchgehend #000",
        }
      })
    );
  }
  
  Inhalt zurückgeben;
};

Online-Demo: codesandbox.io/s/a-naive-v…

Prinzip:

Wie wird dieser Scroll-Effekt erzielt? Zuerst rendern wir einen „Phantom“-Container mit der tatsächlichen Listenhöhe in vListContainer, um dem Benutzer das Scrollen zu ermöglichen. Zweitens hören wir auf das Ereignis „onScroll“ und berechnen dynamisch den Startindex, der dem aktuellen Bildlauf-Offset entspricht (wie viel nach dem Hochscrollen verborgen wird), jedes Mal, wenn der Benutzer den Bildlauf auslöst. Wenn wir feststellen, dass sich der neue Boden von dem aktuell angezeigten Index unterscheidet, weisen wir einen Wert zu und „setState“ löst eine Neuzeichnung aus. Wenn der aktuelle Bildlaufversatz des Benutzers keine Indexaktualisierung auslöst, verfügt die virtuelle Liste aufgrund der Länge des Phantoms selbst über die gleiche Bildlauffunktion wie eine normale Liste. Wenn das Neuzeichnen ausgelöst wird, kann der Benutzer das Neuzeichnen der Seite nicht wahrnehmen, da wir den Startindex berechnen (da der nächste Frame des aktuellen Bildlaufs mit dem von uns neu gezeichneten Inhalt übereinstimmt).

Optimierung:

Bei der virtuellen Liste, die wir oben implementiert haben, lässt sich unschwer feststellen, dass die Liste flackert bzw. nicht rechtzeitig gerendert wird oder leer ist, sobald ein schnelles Wischen durchgeführt wird. Erinnern Sie sich, was wir am Anfang gesagt haben: die maximale Anzahl sichtbarer Zeilen zum Rendern für Benutzer + „BufferSize“? Für den eigentlichen Inhalt, den wir rendern, können wir das Konzept des Puffers hinzufügen (das heißt, mehr Elemente nach oben und unten rendern, um das Problem zu lösen, dass beim schnellen Gleiten nicht genügend Zeit zum Rendern bleibt). Die optimierte onScroll-Funktion lautet wie folgt:

beimScrollen(evt: beliebig) {
  ........
  // Den aktuellen StartIndex berechnen
  const currentStartIndex = Math.floor(scrollTop / rowHeight);
    
  // Wenn currentStartIndex sich von startIndex unterscheidet (wir müssen die Daten aktualisieren)
  wenn (aktuellerStartIndex !== originStartIdx) {
    // Beachten Sie, dass wir eine neue Variable namens originStartIdx eingeführt haben, die dieselbe Rolle wie startIndex spielt.
    //Gleicher Effekt, zeichnen Sie den aktuellen realen Startindex auf.
    this.originStartIdx = aktuellerStartIndex;
    //Führen Sie eine Header-Pufferberechnung für startIndex durch. this.startIndex = Math.max(this.originStartIdx - bufferSize, 0);
    // Tail-Buffer-Berechnung für endIndex durchführen this.endIndex = Math.min(
      dies.originStartIdx + dies.limit + Puffergröße,
      insgesamt - 1
    );

    Dies.setState({ scrollTop: scrollTop });
  }
}

Online-Demo: codesandbox.io/s/A-better-…

0x2 Listenelement Höhenanpassung

Nachdem wir nun eine virtuelle Liste mit Elementen „fester Höhe“ implementiert haben, was passiert, wenn wir auf ein Geschäftsszenario stoßen, in dem die Höhe der Liste nicht festgelegt und sie sehr lang ist?

  • Im Allgemeinen gibt es drei Möglichkeiten, virtuelle Listen zu implementieren, wenn Listenelemente mit unbestimmter Höhe auftreten:

1. Ändern Sie die Eingabedaten und übergeben Sie die Höhe, die jedem Element entspricht dynamicHeight[i] = xx ist die Zeilenhöhe des Elements i

Die Höhe jedes Elements muss bekannt sein (unpraktisch)

2. Zeichnen Sie das aktuelle Element zuerst außerhalb des Bildschirms und richten Sie die Höhe für die Messung aus, bevor Sie es in den sichtbaren Bereich des Benutzers rendern

Diese Methode entspricht einer Verdoppelung der Rendering-Kosten (nicht praktikabel)

3. Übergeben Sie eine estimateHeight-Eigenschaft, um zunächst die Zeilenhöhe zu schätzen und darzustellen. Rufen Sie dann nach Abschluss des Renderings die tatsächliche Zeilenhöhe ab und aktualisieren und zwischenspeichern Sie diese.

Es werden zusätzliche Transformationen eingeführt (akzeptabel) und ich werde später erklären, warum zusätzliche Transformationen erforderlich sind …

  • Kommen wir noch einmal kurz auf den HTML-Teil zurück.
<!--ver 1.0 -->
<div Klassenname="vListContainer">
  <div Klassenname="Phantominhalt">
    ...
    <!-- Artikel-1 -->
    <!-- Artikel-2 -->
    <!-- Artikel-3 -->
    ....
  </div>
</div>


<!--ver 1.1 -->
<div Klassenname="vListContainer">
  <div Klassenname = "Phantominhalt" />
  <div Klassenname="tatsächlicher Inhalt">
    ...
    <!-- Artikel-1 -->
    <!-- Artikel-2 -->
    <!-- Artikel-3 -->
    ....
  </div>
</div>
  • Als wir die virtuelle Liste mit „fester Höhe“ implementierten, renderten wir die Elemente im PhantomContent-Container, setzten die Position jedes Elements auf absolut und definierten das obere Attribut gleich i * rowHeight, um sicherzustellen, dass der gerenderte Inhalt immer innerhalb des sichtbaren Bereichs des Benutzers liegt, unabhängig davon, wie er scrollt. Wenn die Listenhöhe unsicher ist, können wir die Y-Position des aktuellen Elements nicht genau über die geschätzte Höhe berechnen. Daher benötigen wir einen Container, der uns bei dieser absoluten Positionierung hilft.
  • actualContent ist unser neu eingeführter Container zur Listeninhaltsdarstellung. Indem wir die Eigenschaft position: absolute für diesen Container festlegen, vermeiden wir, sie für jedes Element festzulegen.
  • Es gibt einen Unterschied, da wir stattdessen den Container „actualContent“ verwenden. Beim Sliden müssen wir dynamisch eine Y-Transformation an der Position des Containers durchführen, damit sich der Container immer im Sichtfeld des Benutzers befindet:
getTransform() {
  const { scrollTop } = dieser.Zustand;
  const { Zeilenhöhe, Puffergröße, Ursprungsstart-Idx } = dies;

  // Aktueller Gleitoffset - Aktuelle gekürzte (nicht vollständig verschwundene) Distanz - Kopfpufferdistanz return `translate3d(0,${
    nach oben scrollen -
    (scrollTop % Zeilenhöhe) –
    Math.min(originStartIdx, Puffergröße) * Zeilenhöhe
  }px,0)`;

}

Online-Demo: codesandbox.io/s/av-list-…

(Hinweis: Wenn kein hoher Grad an Anpassungsfähigkeit vorhanden ist und keine Zellwiederverwendung implementiert ist, ist die Leistung beim Rendern von Elementen in Phantom über Absolute besser als über Transform. Dies liegt daran, dass der Inhalt bei jedem Rendern neu angeordnet wird. Wenn jedoch Transform verwendet wird, entspricht dies (Neuanordnen + Transformieren) > Neuanordnen.)

  • Zurück zur Frage der adaptiven Listenelementhöhe. Da wir nun einen Element-Rendering-Container (actualContent) haben, der ein normales Blocklayout darin ausführen kann, können wir nun den gesamten Inhalt direkt rendern, ohne eine Höhe anzugeben. An den Stellen, an denen wir zuvor „rowHeight“ zur Höhenberechnung verwenden mussten, haben wir es einheitlich durch „ EstimatedHeight“ zur Berechnung ersetzt.

Limit = Math.ceil(Höhe / geschätzte Höhe)
Phantomhöhe = Gesamthöhe * geschätzte Höhe

  • Um zu vermeiden, dass die Höhe jedes Elements nach dem Rendern wiederholt berechnet werden muss (getBoundingClientReact().height), benötigen wir gleichzeitig ein Array zum Speichern dieser Höhen
Schnittstelle CachedPosition {
  Index: Zahl; // Der Index des Elements, das dem aktuellen Post entspricht. Op: Zahl; // Obere Position Bottom: Zahl; // Untere Position Höhe: Zahl; // Elementhöhe Wert: Zahl; // Unterscheidet sich die Höhe von der vorherigen (Schätzung)?}

zwischengespeichertePositionen: ZwischengespeichertePosition[] = [];

// CachedPositions initialisieren
initCachedPositions = () => {
  const { geschätzteZeilenhöhe } = dies;
  diese.cachedPositions = [];
  für (lass i = 0; i < this.total; ++i) {
    diese.cachedPositions[i] = {
      Index: ich,
      Höhe: geschätzteZeilenhöhe, // Benutze geschätzteHöhe, um zuerst zu schätzen. Oben: i * geschätzteZeilenhöhe, // Wie oben. Unten: (i + 1) * geschätzteZeilenhöhe, // Wie oben.
      dWert: 0,
    };
  }
};
  • Nachdem wir cachedPositions berechnet (initialisiert) haben, ist die Höhe des Phantoms der untere Wert des letzten Elements in cachedPositions, da wir die Ober- und Unterseite jedes Elements berechnen.
this.phantomHeight = this.cachedPositions[cachedPositionsLen - 1].bottom;
  • Nachdem wir die Elemente im Ansichtsfenster des Benutzers gemäß estimateHeight gerendert haben, müssen wir die tatsächliche Höhe der gerenderten Elemente aktualisieren. Zu diesem Zeitpunkt können wir den Lifecycle-Hook componentDidUpdate verwenden, um Folgendes zu berechnen, zu beurteilen und zu aktualisieren:
KomponenteDidUpdate() {
  ......
  // actualContentRef muss aktuell vorhanden sein (bereits gerendert) + total muss > 0 sein
  wenn (dieser.aktuelleInhaltsbezug.aktuell && dieses.gesamt > 0) {
    dies.updateCachedPositions();
  }
}

updateCachedPositions = () => {
  // zwischengespeicherte Elementhöhe aktualisieren
  Konstante Knoten: NodeListOf<any> = this.actualContentRef.current.childNodes;
  const start = Knoten[0];

  // Höhenunterschied für jeden sichtbaren Knoten berechnen …
  nodes.forEach((node: HTMLDivElement) => {
    wenn (!Knoten) {
      // zu schnell scrollen?...
      zurückkehren;
    }
    const rect = node.getBoundingClientRect();
    const { Höhe } = Rechteck;
    const index = Zahl(node.id.split('-')[1]);
    const oldHeight = this.cachedPositions[index].height;
    const dValue = oldHeight – Höhe;

    wenn (dWert) {
      this.cachedPositions[index].bottom -= dValue;
      this.cachedPositions[index].height = Höhe;
      this.cachedPositions[index].dValue = dValue;
    }
  });

  // führe ein einmaliges Höhenupdate durch …
  Lassen Sie startIdx = 0;
  
  wenn (Start) {
    startIdx = Zahl(start.id.split('-')[1]);
  }
  
  const cachedPositionsLen = this.cachedPositions.length;
  Lassen Sie cumulativeDiffHeight = this.cachedPositions[startIdx].dValue;
  this.cachedPositions[startIdx].dValue = 0;

  für (lass i = startIdx + 1; i < cachedPositionsLen; ++i) {
    const item = this.cachedPositions[i];
    // Höhe aktualisieren
    diese.cachedPositions[i].oben = diese.cachedPositions[i - 1].unten;
    this.cachedPositions[i].bottom = this.cachedPositions[i].bottom - kumulativeDifferenzhöhe;

    wenn (item.dValue !== 0) {
      kumulativeDifferenzhöhe += item.dValue;
      item.dWert = 0;
    }
  }

  // Aktualisiere die Höhe unseres Phantom-Divs
  const Höhe = this.cachedPositions[cachedPositionsLen - 1].bottom;
  this.phantomHeight = Höhe;
  this.phantomContentRef.current.style.height = `${height}px`;
};
  • Da wir nun die genauen Höhen- und Positionswerte aller Elemente haben, ändern wir die Methode zum Abrufen des Startelements, das dem aktuellen scrollTop (Offset) entspricht, um es über cachedPositions abzurufen:

Da es sich bei unseren cachedPositions um ein geordnetes Array handelt, können wir die binäre Suche verwenden, um den zeitlichen Aufwand bei der Suche zu reduzieren.

getStartIndex = (scrollTop = 0) => {
  let idx = binarySearch<CachedPosition, Zahl>(this.cachedPositions, scrollTop, 
    (aktuellerWert: ZwischengespeichertePosition, Zielwert: Zahl) => {
      const currentCompareValue = aktuellerWert.unten;
      wenn (aktuellerVergleichswert === Zielwert) {
        gibt CompareResult.eq zurück;
      }

      if (aktuellerVergleichswert < Zielwert) {
        gibt CompareResult.lt zurück;
      }

      gibt CompareResult.gt zurück;
    }
  );

  const targetItem = this.cachedPositions[idx];

  // Geben Sie uns im Falle einer binären Suche nicht sichtbare Daten (ein IDX des aktuell sichtbaren Werts – 1) …
  wenn (Zielelement.unten < scrollTop) {
    idx += 1;
  }

  idx zurückgeben;
};

  

beimScroll = (evt: beliebig) => {
  wenn (evt.target === dieser.scrollingContainer.current) {
    ....
    const currentStartIndex = this.getStartIndex(scrollTop);
    ....
  }
};
  • Implementierung der binären Suche:
export enum Vergleichsergebnis {
  Gleichung = 1,
  es,
  gt,
}



Exportfunktion binäreSuche<T, VT>(Liste: T[], Wert: VT, Vergleichsfunktion: (aktuell: T, Wert: VT) => Vergleichsergebnis) {
  lass start = 0;
  let end = Listenlänge - 1;
  lass tempIndex = null;

  während (Start <= Ende) {
    tempIndex = Math.floor((Start + Ende) / 2);
    const mittlerer Wert = Liste[tempIndex];
    const compareRes: CompareResult = Vergleichsfunktion (mittlerer Wert, Wert);

    wenn (compareRes === CompareResult.eq) {
      gibt TempIndex zurück;
    }
    
    wenn (compareRes === CompareResult.lt) {
      Start = Temperaturindex + 1;
    } sonst wenn (compareRes === CompareResult.gt) {
      Ende = TempIndex - 1;
    }
  }

  gibt TempIndex zurück;
}
  • Schließlich wird die Methode zum Abrufen der Transformation nach dem Scrollen wie folgt transformiert:
getTransform = () =>
    `translate3d(0,${this.startIndex >= 1 ? this.cachedPositions[this.startIndex - 1].bottom : 0}px,0)`;

Online-Demo: codesandbox.io/s/av-list-…

Oben finden Sie Einzelheiten zur Implementierung einer hochadaptiven virtuellen Liste in React. Weitere Informationen zur adaptiven virtuellen Liste von React finden Sie in den anderen verwandten Artikeln auf 123WORDPRESS.COM!

Das könnte Sie auch interessieren:
  • Detaillierte Erläuterung des Betriebs des virtuellen DOM zur Simulation des React View-Renderings
  • Eine kurze Diskussion über das größte Highlight von React: Virtual DOM
  • Detaillierte Erklärung des virtuellen DOM und des Diff-Algorithmus in React

<<:  Lösung für langsame Netzwerkanforderungen im Docker-Container

>>:  Detailliertes Tutorial zur Installation und Deinstallation von MySql

Artikel empfehlen

Entwicklungshandbuch für Chrome-Plugins (Erweiterungen) (vollständige Demo)

Inhaltsverzeichnis Vorne geschrieben Vorwort Was ...

Linux-Betrieb und -Wartung – Tutorial zur grundlegenden Datenträgerverwaltung

1. Festplattenpartition: 2. fdisk-Partition Wenn ...

JS implementiert einen einfachen Zähler

Verwenden Sie HTML, CSS und JavaScript, um einen ...

MySQL-Datenbank muss SQL-Anweisungen kennen (erweiterte Version)

Dies ist eine erweiterte Version. Die Fragen und ...

Funktionsweise von SQL-SELECT-Datenbankabfragen

Obwohl wir keine professionellen DBAs sind, könne...

JavaScript-Code zur Implementierung der Weibo-Batch-Unfollow-Funktion

Ein cooler JavaScript-Code, um Weibo-Benutzern st...

Zusammenfassung der Konstruktor- und Superwissenspunkte in React-Komponenten

1. Einige Tipps zu mit class in react deklarierte...

Linux-Unlink-Funktion und wie man Dateien löscht

1. Unlink-Funktion Bei Hardlinks wird mit „unlink...

Die Lösung von html2canvas, dass Bilder nicht normal erfasst werden können

Frage Lassen Sie mich zunächst über das Problem s...

Implementierungsbeispiel für den Linux-Befehl „tac“

1. Befehlseinführung Der Befehl tac (umgekehrte R...

Detaillierte Erklärung der sieben Datentypen in JavaScript

Inhaltsverzeichnis Vorwort: Detaillierte Einführu...

Erstellen eines sekundären Menüs mit JavaScript

In diesem Artikelbeispiel wird der spezifische Ja...