React Diff-Algorithmus-Quellcodeanalyse

React Diff-Algorithmus-Quellcodeanalyse

Der Diff-Algorithmus in React wird auch als Abstimmungsalgorithmus bezeichnet. Die zugehörige Funktion heißt reconcileChildren . Ihre Hauptfunktion besteht darin, diejenigen Elemente zu markieren, die sich während des Aktualisierungsvorgangs geändert haben. Zu diesen Änderungen zählen das Hinzufügen, Verschieben und Löschen. Der Vorgang findet in beginWork statt und „Diff“ wird nur ausgeführt, wenn es sich nicht um das erste Rendering handelt.

Ich habe schon einige Artikel gelesen, in denen der Diff-Algorithmus als Vergleich zweier Fiber-Bäume beschrieben wird. Das ist falsch. Der eigentliche Diff-Prozess ist ein Vergleich einer Reihe vorhandener Fiber-Knoten und eines neuen, von JSX generierten ReactElement , und dann wird ein neuer Fiber-Knoten generiert. In diesem Prozess wird auch versucht, die vorhandenen Fiber-Knoten wiederzuverwenden.

Node Diff ist in zwei Typen unterteilt:

  1. Einzelner Knoten Diff - Element , Portal , string , number .
  2. Multi-Node-Diff - Array , Iterator .

Die folgende React-Version ist 17.0.1 und die Codedatei ist ReactChildFiber.old.js.

Einzelner Knoten Diff

Single-Node-Diff ist relativ einfach. Es wird nur versucht, den Knoten wiederzuverwenden, wenn Schlüssel und Typ gleich sind, andernfalls wird ein neuer Knoten zurückgegeben.

In den meisten Fällen weisen wir einzelnen Knoten keine Schlüssel zu, daher ist der Standardwert null, was dasselbe ist.

Einzelelement abgleichen

  // Einzelknotenvergleichsfunktion reconcileSingleElement(
    returnFiber: Faser,
    currentFirstChild: Fiber | null,
    Element: ReactElement,
    Fahrspuren: Fahrspuren,
  ): Faser {
    // Der Schlüssel des neuen reactElement
    const Schlüssel = Element.Schlüssel;
    //Aktueller untergeordneter Faserknoten let child = currentFirstChild;
    während (Kind !== null) {
      // Nur unterscheiden, wenn der Schlüssel derselbe ist
      wenn (Kind.Schlüssel === Schlüssel) {
        Schalter (untergeordnetes Tag) {
          // ...
          Standard: {
            // Wenn die aktuelle Faser und das ReactElement den gleichen Typ haben, dann wenn (child.elementType === element.type) {
              // Andere Knoten auf derselben Ebene löschen deleteRemainingChildren(returnFiber, child.sibling);
              // Aktuelle untergeordnete Faser erneut verwenden
              const vorhanden = useFiber(Kind, Element.Eigenschaften);
              bestehend.ref = coerceRef(returnFiber, Kind, Element);
              bestehend.return = returnFiber;
              // Eine wiederverwendbare untergeordnete Faser zurückgeben
              bestehende zurückgeben;
            }
            brechen;
          }
        }
        // Nicht übereinstimmende Löschknoten deleteRemainingChildren(returnFiber, child);
        brechen;
      } anders {
        //Knoten direkt löschen, wenn der Schlüssel unterschiedlich ist deleteChild(returnFiber, child);
      }
      Kind = Kind.Geschwister;
    }

    // Neuer Fiber-Knoten const erstellt = createFiberFromElement(element, returnFiber.mode, lanes);
    erstellt.ref = coerceRef(returnFiber, aktuellesErstesKind, Element);
    erstellt.return = returnFiber;
    Rückgabe erstellt;
  }

Multi-Node-Diff

Der Quellcode unterteilt mehrere Knoten in Array-Knoten und iterierbare Knoten.

wenn (istArray(neuesKind)) {
  returniere versöhnlichesKinderArray(
    Rückkehrfaser,
    aktuellesErstesKind,
    neues Kind,
    Fahrspuren,
  );
}

wenn (getIteratorFn(neuesKind)) {
  returniere reconcileChildrenIterator(
    Rückkehrfaser,
    aktuellesErstesKind,
    neues Kind,
    Fahrspuren,
  );
}

Die entsprechenden Diff-Funktionen sind reconcileChildrenArray und reconcileChildrenIterator . Ihre grundlegende Diff-Logik ist dieselbe, daher wird nur der Diff der Array-Knoten analysiert – die Funktion reconcileChildrenArray .

Dieser Codeabschnitt ist relativ lang, aber die Logik ist sehr klar und er ist ab der Trennlinie in zwei Durchlaufrunden unterteilt.

  • Die erste Durchlaufrunde besteht aus den Knoten mit der gleichen Reihenfolge und dem gleichen Schlüssel, die aktualisiert werden müssen.
  • Die zweite Durchlaufrunde besteht aus Knoten mit unterschiedlicher Reihenfolge und möglicherweise unterschiedlichen Schlüsseln. Diese Knoten müssen hinzugefügt, verschoben oder gelöscht werden.

Die erste Durchlaufrunde gilt nur für den Fall, dass Schlüssel und Reihenfolge gleich sind. Die Positionen der Knoten, die diesen Schlüsseln entsprechen, haben sich nicht geändert, und es ist nur ein Aktualisierungsvorgang erforderlich. Sobald beim Durchlauf ein anderer Schlüssel gefunden wird, muss aus der Schleife ausgestiegen werden.

// Alter Knoten <li key="0"/>
<li-Taste="1"/>
<li-Taste="2"/>
// Neuer Knoten <li key="0"/>
<li-Taste="1"/>
<li-Taste="5"/>

// key="5" ist anders, springe aus der Durchquerung // Die erste Runde der Durchquerung Knoten <li key="0"/>
<li-Taste="1"/>
// <li key="2"/> und <li key="5"/> verbleiben in der zweiten Runde der Durchquerung und des Vergleichs.

Nach der ersten Durchquerungsrunde gibt es zwei Situationen.

  1. Die Anzahl der neuen Knoten ist geringer als die Anzahl der alten Knoten. Zu diesem Zeitpunkt müssen die redundanten alten Knoten zum Löschen markiert werden.
  2. Die Anzahl der neuen Knoten ist größer als die Anzahl der alten Knoten. In diesem Fall müssen die zusätzlichen neuen Knoten als neu hinzugefügt gekennzeichnet werden.

Die zweite Durchlaufrunde erfolgt für unterschiedliche Schlüssel oder unterschiedliche Reihenfolgen. Die möglichen Situationen sind wie folgt:

// Alter Knoten <li key="0"/>
<li-Taste="1"/>
<li-Taste="2"/>
// Neuer Knoten <li key="0"/>
<li-Taste="2"/>
<li-Taste="1"/>

// Die zweite Durchlaufrunde vergleicht die beiden Knoten <li key="2"/> und <li key="1"/>

Die zweite Durchquerungsrunde wird etwas komplizierter sein und später ausführlicher besprochen.

Der detaillierte Code lautet wie folgt.

KinderArray abgleichen

  Funktion reconcileChildrenArray(
    returnFiber: Faser,
    currentFirstChild: Fiber | null,
    neueUntergeordnete: Array<*>,
    Fahrspuren: Fahrspuren,
  ): Faser | null {
    // Von der Funktion let resultingFirstChild zurückgegebener Fiber-Knoten: Fiber | null = null;
    let previousNewFiber: Fiber | null = null;

    // oldFiber ist eine verknüpfte Liste let oldFiber = currentFirstChild;
    // Wird verwendet, um zu bestimmen, ob der Knoten verschoben wird. let lastPlacedIndex = 0;
    sei newIdx = 0;
    let nextOldFiber = null;
    // In der ersten Durchlaufrunde werden nur Knoten mit dem gleichen Schlüssel durchlaufen for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
      wenn (alterFiber.index > neueIdx) {
        nächsteAlteFiber = alteFiber;
        alteFiber = null;
      } anders {
        // Jedes Mal, wenn der alte Fiber-Knoten durchlaufen wird, zeigt er auf das Geschwisterelement, das der Fiber-Knoten der nächsten Schleife ist. nextOldFiber = oldFiber.sibling;
      }
      // Wenn der Schlüssel derselbe ist, gib den Faserknoten zurück; wenn der Schlüssel unterschiedlich ist, gib null zurück
      // Wenn der Typ derselbe ist, den Knoten erneut verwenden, andernfalls einen neuen Knoten zurückgeben const newFiber = updateSlot(
        Rückkehrfaser,
        alteFaser,
        neueKinder[neueIdx],
        Fahrspuren,
      );
      // newFiber ist null, was darauf hinweist, dass der Schlüssel unterschiedlich ist. Die Schleife wird beendet, wenn (newFiber === null) {
        wenn (alteFaser === null) {
          alteFaser = nächstealteFaser;
        }
        brechen;
      }
      // Wenn newFiber.alternate null ist, handelt es sich um einen neuen Knoten, was darauf hinweist, dass ein neuer Fiber-Knoten mit einem anderen Typ erstellt wird, wenn (oldFiber && newFiber.alternate === null) {
        // Das oldFiber-Tag muss gelöscht werden deleteChild(returnFiber, oldFiber);
      }
      // Platziere den Knoten und aktualisiere den lastPlacedIndex
      letzterPlatzierterIndex = OrtKind(neueFaser, letzterPlatzierterIndex, neueIdx);
      // Bilde eine neue Fiber-Knotenkette if (previousNewFiber === null) {
        resultierendesErstesKind = neueFaser;
      } anders {
        vorherigeNeueFiber.geschwister = neueFiber;
      }
      vorherigeNeueFiber = neueFiber;
      alteFaser = nächstealteFaser;
    }

    /*
    Nach der ersten Durchlaufrunde ist die Anzahl der neuen Knoten geringer als die Anzahl der alten Knoten. newChildren wurde durchlaufen und die verbleibenden Faserknoten werden gelöscht. Die mögliche Situation ist wie folgt ⬇️
    Vorher <li key="0"/>
    <li-Taste="1"/>
    <li-Taste="2"/>
    Neu <li key="0"/>
    <li-Taste="1"/>
    <li key="2"/> wird gelöscht*/
    wenn (newIdx === neueKinder.Länge) {
      löscheRestKinder(returnFiber, oldFiber);
      gib das resultierendeErsteKind zurück;
    }

    /*
    Nach der ersten Durchlaufrunde ist die Anzahl der neuen Knoten größer als die Anzahl der alten Knoten. OldFiber wurde durchlaufen. Die mögliche Situation ist wie folgt ⬇️
    Vorher <li key="0"/>
    <li-Taste="1"/>
    Neu <li key="0"/>
    <li-Taste="1"/>
    <li-Taste="2"/>
    Ein neuer <li key="2"/> wird hinzugefügt. Dieser Abschnitt ist die Einfügelogik des neuen Knotens*/
    wenn (alteFaser === null) {
      für (; newIdx < newChildren.length; newIdx++) {
        const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
        wenn (newFiber === null) {
          weitermachen;
        }
        letzterPlatzierterIndex = OrtKind(neueFaser, letzterPlatzierterIndex, neueIdx);
        // Bilde eine neue Fiber-Knotenkette if (previousNewFiber === null) {
          resultierendesErstesKind = neueFaser;
        } anders {
          vorherigeNeueFiber.geschwister = neueFiber;
        }
        vorherigeNeueFiber = neueFiber;
      }
      gib das resultierendeErsteKind zurück;
    }
      
    // ---------------------------------------------------------------------

    // Verwende die verbleibende oldFiber, um eine Schlüssel->Fiber-Knoten-Map zu erstellen, sodass der entsprechende alte Fiber-Knoten über den Schlüssel const existingChildren = mapRemainingChildren(returnFiber, oldFiber); abgerufen werden kann.
    
    // Zweite Durchlaufrunde, fahren Sie mit dem Durchlaufen der verbleibenden Knoten fort. Diese Knoten müssen möglicherweise verschoben oder gelöscht werden für (; newIdx < newChildren.length; newIdx++) {
      // Den alten Knoten entsprechend dem Schlüssel aus der Karte abrufen und den aktualisierten neuen Knoten zurückgeben const newFiber = updateFromMap(
        bestehendeKinder,
        Rückkehrfaser,
        neueIdx,
        neueKinder[neueIdx],
        Fahrspuren,
      );
      wenn (neueFiber !== null) {
        // Den neuen Knoten wiederverwenden und den alten Knoten aus der Karte löschen. Die entsprechende Situation kann eine Positionsänderung sein, wenn (newFiber.alternate !== null) {
          // Die wiederverwendeten Knoten müssen aus der Karte entfernt werden, da die verbleibenden Knoten in der Karte als Löschung markiert werden existingChildren.delete(
            newFiber.key === null ?
          );
        }
        // Platzieren Sie den Knoten und bestimmen Sie, ob er verschoben werden soll lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        wenn (vorherigeNeueFiber === null) {
          resultierendesErstesKind = neueFaser;
        } anders {
          vorherigeNeueFiber.geschwister = neueFiber;
        }
        vorherigeNeueFiber = neueFiber;
      }
    }

    // Löschen Sie die verbleibenden nutzlosen Knoten existingChildren.forEach(child => deleteChild(returnFiber, child));

    gib das resultierendeErsteKind zurück;
  }

Die erste Durchquerungsrunde ist relativ einfach zu verstehen. Hier werden wir die zweite Durchquerungsrunde im Detail analysieren, da in der zweiten Runde die Frage aufgeworfen wird, ob die Wiederverwendung verschoben werden muss.

In der zweiten Durchlaufrunde werden zunächst die verbleibenden oldFiber durchlaufen und eine key -> 舊fiber節點的Map erstellt, mit der alte Knoten schnell über Schlüssel abgerufen werden können.

Die folgende Durchquerung verwendet immer noch den neuen Knoten als Durchquerungsobjekt. Bei jeder Durchquerung wird der Schlüssel des neuen Knotens verwendet, um den alten Knoten aus der Map zu entfernen und zu vergleichen, ob er wiederverwendet werden kann. Die entsprechende Funktion ist updateFromMap .

Wenn der Knoten ein alternate Attribut hat, ist er ein wiederverwendeter Knoten. Zu diesem Zeitpunkt muss er aus existingChildren entfernt werden. Anschließend werden die Knoten, die nach der zweiten Durchlaufrunde noch in existingChildren vorhanden sind, als gelöscht markiert.

Wie kann festgestellt werden, ob ein Knoten verschoben wurde?

Hier gibt es eine Variable lastPlacedIndex um zu bestimmen, ob der Knoten verschoben wurde. Dieser Wert wird jedes Mal aktualisiert, wenn ein Knoten zu einer neuen Fiber-verknüpften Liste hinzugefügt wird.

Wenn oldIndex des wiederverwendeten Knotens kleiner als lastPlacedIndex ist, wird er verschoben. Wenn er nicht verschoben werden muss, wird lastPlacedIndex auf den größeren oldIndex aktualisiert und der nächste Knoten wird nach dem neuen Wert beurteilt. Der Code lautet wie folgt:

Funktion OrtKind(
  newFiber: Faser,
  lastPlacedIndex: Nummer,
  newIndex: Nummer,
): Nummer {
  newFiber.index = neuerIndex;
  const current = neueFiber.alternate;
  wenn (aktuell !== null) {
    const alterIndex = aktueller.index;
    if (alterIndex < letzterplatzierterIndex) {
 			// Knotenbewegung newFiber.flags = Platzierung;
      gibt letzten platzierten Index zurück;
    } anders {
      // Die Knotenposition hat sich nicht geändert return oldIndex;
    }
  } anders {
    // Neuen Knoten einfügen newFiber.flags = Placement;
    gibt letzten platzierten Index zurück;
  }
}

Zum Beispiel:

// altes abcd
// Neues acbd

abcd sind alles Schlüsselwerte.

Nach der ersten Durchlaufrunde müssen die verbleibenden Knoten verglichen werden:

// Alter BCD
// Neues CBD

Knoten a wurde in der ersten Runde wiederverwendet und placeChild wurde aufgerufen. Zu diesem Zeitpunkt ist der lastPlacedIndex-Wert 0.

Beim Eintritt in die zweite Durchlaufrunde ist der neue Knoten immer noch das Durchlaufobjekt.

c => existiert im alten Knoten und kann wiederverwendet werden. Sein Index im alten Knoten ist 2. 2 > lastPlacedIndex(0). Kein Verschieben nötig. Weisen Sie lastPlacedIndex den Wert 2 zu.
b => existiert im alten Knoten und kann wiederverwendet werden. Sein Index im alten Knoten ist 1, 1 < lastPlacedIndex(2). Es muss verschoben und mit Placement markiert werden.
d => existiert im alten Knoten und kann wiederverwendet werden. Sein Index im alten Knoten ist 3, 3 > lastPlacedIndex(2), also muss er nicht verschoben werden.

Anhand dieses Beispiels können wir erkennen, dass in React die Knoten auf der rechten Seite, die nicht verschoben werden müssen, als Referenzen verwendet werden und die Knoten, die verschoben werden müssen, alle gleichmäßig von links nach rechts verschoben werden.

Im darauffolgenden Layout-Schritt werden die mit Placement markierten Knoten insertBefore .

Zusammenfassen

Der Kerncode des Diff-Algorithmus in React ist nicht sehr lang, aber die Einführung des Schlüssels ändert die Komplexität geschickt von O(n3) auf O(n).

Der interne Wettbewerb unter den Programmierern ist zu groß, also muss ich den Quellcode lernen.

Oben finden Sie den detaillierten Inhalt der Quellcodeanalyse des React-Diff-Algorithmus. Weitere Informationen zum React-Diff-Algorithmus finden Sie in den anderen verwandten Artikeln auf 123WORDPRESS.COM!

Das könnte Sie auch interessieren:
  • Kennen Sie den Diff-Algorithmus in React?
  • Detaillierte Analyse des Diff-Algorithmus in React
  • Detaillierte Erklärung des virtuellen DOM und des Diff-Algorithmus in React
  • Eine kurze Diskussion zur Analyse des Diff-Algorithmus aus dem React-Rendering-Prozess
  • Beispielimplementierung des React-Diff-Algorithmus
  • Ideen zur Implementierung des React-Diff-Algorithmus und Prinzipanalyse

<<:  Detaillierte Erklärung zur Verwendung des Alias-Befehls unter Linux

>>:  So beheben Sie den Fehler „MySQL-Dienst kann nicht gestartet werden, Fehler 1069“

Artikel empfehlen

Werfen wir einen Blick auf die Vorkompilierung von JavaScript (Zusammenfassung)

JS-Lauftrilogie js-Ausführungscode ist in drei Sc...

Serielle und parallele Operationen in JavaScript

Inhaltsverzeichnis 1. Einleitung 2. es5-Methode 3...

JavaScript-Tippspiel

In diesem Artikel wird der spezifische JavaScript...

Zusammenfassung der Linux Logical Volume Management (LVM)-Nutzung

Die Verwaltung des Speicherplatzes ist für System...

Detaillierte Anweisungen zur Installation von Jenkins auf Ubuntu 16.04

1. Voraussetzungen JDK wurde installiert echo $PA...

Lösung für den Fehler beim Importieren von MySQL Big Data in Navicat

Die von Navicat exportierten Daten können nicht i...

So stellen Sie ein Vue-Projekt mit Docker-Image + nginx bereit

1. Vue-Projekt verpacken Geben Sie den folgenden ...

Docker-Installation Nginx Tutorial Implementierung Abbildung

Lassen Sie uns Nginx installieren und ausprobiere...

Alibaba Cloud Server Ubuntu Konfigurations-Tutorial

Da für den Import benutzerdefinierter Ubuntu-Imag...

js implementiert Array-Abflachung

Inhaltsverzeichnis So reduzieren Sie ein Array 1....