Startseite / Artikel / Wo useState seinen Wert speichert: React-Elemente gegen Fibers

Wo useState seinen Wert speichert: React-Elemente gegen Fibers

Warum ein React-Funktionskomponente alles zwischen den Aufrufen vergisst, warum Elemente keinen Zustand speichern können und wie das memoizedState-Feld der Fiber die Werte von useState am Leben erhält.

1724 Wörter

Ein Funktionskomponente ist im Grunde nur eine Funktion, und Funktionen vergessen ihre lokalen Variablen sofort, sobald sie zurückgegeben werden. Doch useState gibt den aktualisierten Wert bei jeder Neuzeichnung zurück, als würde die Funktion sich daran erinnern. Dieser Text beantwortet präzise eine einzige, enge Frage: Wo befindet sich dieser Wert physisch zwischen den Neuzeichnungen? Am Ende werden Sie in der Lage sein, die beiden Objekte zu unterscheiden, die React für jede Komponente erstellt – das Element und den Fiber – sowie zu erklären, welches von ihnen den Zustand enthält und warum.

Das Rätsel: Eine Funktion ohne Gedächtnis

Fangen wir mit der vertrautesten Komponente überhaupt an – einem Zähler:

function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

Klicken Sie einmal – es wird 1 angezeigt, klicken Sie erneut – es wird 2 angezeigt. Nichts Überraschendes, bis man es als gewöhnlichen JavaScript-Code betrachtet. Counter ist eine Funktion. Jedes Mal, wenn eine Funktion aufgerufen wird, werden ihre lokalen Variablen aus dem Nichts erstellt, und wenn sie zurückgegeben werden, sind sie verschwunden. Eine weitere Aufrufung beginnt völlig neu.

Nach dieser Logik sollte die Zeile useState(0) beim zweiten Aufruf von React auf Counter() erneut 0 erzeugen. Tatsächlich wird jedoch 1 erzeugt. Irgendetwas außerhalb der Funktion muss die Zahl vom einen Aufruf zum nächsten speichern. Der Funktionskörper kann es nicht sein, denn so verhalten sich Funktionen einfach nicht. Was ist es dann?

Ausschluss des Elements

Es gibt einen offensichtlichen Kandidaten, der sich als falsch herausstellt – seine Eliminierung macht die wahre Antwort klarer.

Die Zeile <button onClick={...}>{count}</button> ist JSX. Browser führen JSX niemals aus; ein Compiler wie Babel oder der TypeScript-Compiler überschreibt es in einen gewöhnlichen Funktionsaufruf, bevor der Code bereitgestellt wird. Konzeptionell sieht das Ergebnis so aus:

React.createElement("button", { onClick: fn }, count)

Durch die in React 17 eingeführte automatische JSX-Runtime ist der kompilierte Aufruf tatsächlich jsx() aus react/jsx-runtime anstelle von React.createElement, doch beides erfüllt denselben Zweck. Unabhängig davon, wie die Funktion aufgerufen wird, gibt sie ein gewöhnliches Objekt zurück:

{
  type: "button",
  props: { onClick: fn, children: 1 },
}

React bezeichnet dies als Element. Es handelt sich dabei um eine Beschreibung, nicht um ein tatsächliches Objekt: ein kurzer Eintrag, der angibt, dass hier eine Schaltfläche erscheinen soll, mit diesem Klick-Handler und unter Anzeige dieser Zahl. Reale Elemente enthalten außerdem weitere Felder wie key, ref sowie einen internen $typeof-Marker, doch keines dieser Felder speichert Historie.

Nun die entscheidende Beobachtung: jeder Aufruf von Counter erzeugt ein völlig neues Element. Wenn man auf die Schaltfläche klickt, wird Counter() ausgeführt, ein neues Objekt zurückgegeben und das vorherige wird als „Abfall“ behandelt. Würde der Zustand im Element gespeichert, gäbe es keine Möglichkeit, von „render zwei“ auf „render eins“ überzugehen, da das Objekt von „render eins“ bereits nicht mehr existiert, wenn „render zwei“ beginnt.

Diese Verwerflichkeit ist beabsichtigt. Die Elemente sind billig, weil sie bei jeder Darstellung von Grund auf neu erstellt werden und niemals mit irgendetwas synchronisiert werden müssen. Das bedeutet auch, dass sie nicht dort sein können, wo count gespeichert wird.

Das Objekt, das überlebt: die Fiber

Neben den Elementen behält React für jedes geladene Komponente ein weiteres, länger lebendiges Objekt bei. Dieses wird nicht bei jeder Darstellung neu erstellt, sondern erst beim ersten Laden der Komponente angelegt und anschließend solange aufrechterhalten sowie aktualisiert, wie die Komponente auf dem Bildschirm bleibt. Das ist die Fiber.

Fasern werden oft als etwas Geheimnisvolles beschrieben, doch auf grundlegendster Ebene ist eine Faser einfach ein JavaScript-Objekt. Hinter ihr verbirgt sich keine spezielle Laufzeitkonstruktion. Wenn man in einem Debugger innerhalb von Reacts Reconciler anhält und eine Faser untersucht, findet man ein gewöhnliches Objekt mit einer Reihe von Eigenschaften – genau die Art von Objekten, die man selbst mit geschweiften Klammern erstellen könnte. Man kann sie auch direkt im Browser betrachten: React weist den DOM-Node-Objekten interne Schlüssel zu, die auf ihre Fasern verweisen; so finden React DevTools sie.

Anstatt alle Felder auf einmal aufzulisten, ist es hilfreich, die Faser für Counter nach und nach, ein Feld nach dem anderen, zu erstellen. Unmittelbar nach dem Mount sieht eine minimale Version wie folgt aus:

{
  type: Counter,
}

type: wessen Buchhaltung dies ist

type ist das einfachste Feld. Es gibt an, welchen Komponenten diese Fiber folgt. type: Counter bedeutet, dass das Objekt dazu dient, eine Instanz von Counter zu verwalten und dabei einen Verweis auf die Funktion selbst enthält.

Auch Host-Elemente erhalten Fibers. Die Schaltfläche, die von Counter dargestellt wird, hat ihre eigene Fiber, wobei ihr type der String "button" ist und nicht eine Funktion. Somit hat die <Counter />-Fiber den Wert { type: Counter } und die <button>-Fiber den Wert { type: "button" }: dasselbe Feld, derselbe Zweck, wobei es entweder auf eine eigene Komponente oder auf ein eingebautes Tag verweist.

type spielt ebenfalls eine Rolle bei der Vereinbarung von Zuständen. Wenn React ein neues Element mit einem vorhandenen Fiber an derselben Position vergleicht, ermöglicht ein übereinstimmender type die Wiederverwendung des Fibers und seines Zustands, während ein anderer type dazu führt, dass das alte Fiber weggeworfen und ein neues erstellt wird. Deshalb setzt das Wechseln zwischen zwei verschiedenen Komponenten am selben Ort ihren Zustand zurück.

Auf sich allein gestellt sagt type jedoch nichts darüber aus, wie der Zustand gespeichert wird. Dafür ist das nächste Feld notwendig.

memoizedState: wo die Zahl tatsächlich gespeichert ist

useState benötigt einen Ort außerhalb der Funktion, um den aktuellen Wert zu speichern, da die lokalen Variablen der Funktion bei jedem Aufruf zurückgesetzt werden. Dieser Ort ist ein Feld im Fiber mit dem Namen memoizedState. „Memoisiert“ bedeutet einfach gespeichert: Der Wert wird aus früheren Aufrufen übernommen anstatt neu berechnet.

Wenn man dies zum Skizzenbild hinzufügt, erhält man:

{
  type: Counter,
  memoizedState: { count: 0 },
}

Das ist eine vereinfachte Darstellung. In der tatsächlichen Implementierung verweist memoizedState in der Fiber eines Funktionskomponenten auf das erste Element einer verknüpften Liste von Hook-Objekten – jeweils eines pro Hook-Aufruf – und die Zahl 0 befindet sich im eigenen memoizedState-Feld dieses Hooks anstelle in einem { count: 0 }-Objekt. React hat keine Ahnung, dass Ihre Variable count genannt wird; es kennt lediglich „den Wert des ersten Hooks“. Um zu verstehen, wo der Zustand gespeichert ist, reicht die vereinfachte Version aus.

Klicken Sie nun. React ruft Counter() erneut auf. Wenn die Ausführung bei useState(0) ankommt, gibt der Hook in Ihrem Code nicht den Wert 0 zurück. Stattdessen wird der gespeicherte Zustand der Fiber abgerufen, der aktuelle Wert gefunden (der nach Verarbeitung des Klicks 1 ist) und dieser zurückgegeben. Der Argument für useState dient lediglich als Anfangswert: Er wird beim ersten Render verwendet und danach ignoriert. Von da an ist die Fiber die Quelle der Wahrheit – nicht der Literale im Funktionskörper.

Das ist die vollständige Antwort auf das Einstiegsrätsel. Die Zählung befindet sich weder in der Funktion, die alles zwischen den Aufrufen vergisst, noch im Element, das nach jedem Render verworfen wird. Sie existiert in der Fiber – einem separaten Objekt, das React bei jedem Render von Counter aufrechterhält und aktualisiert.

Zwei praktische Konsequenzen

Dieses Modell erklärt einige Verhaltensweisen, die sonst willkürlich erscheinen:

  • Das Ändern des Arguments von useState nach dem Mount hat keinen Einfluss auf den gespeicherten Wert, da React diesen nur beim ersten Mal liest. Wenn Sie den Zustand aus Props zurücksetzen müssen, ändern Sie die key der Komponente, damit React eine neue Fiber erstellt.
  • Weil die Fiber nach ihrer Position im Baum aufgesucht wird, gehört der Zustand dazu, wo eine Komponente dargestellt wird, und nicht zur Funktionsdefinition. Zwei <Counter />-Elemente nebeneinander erhalten zwei Fibers und zwei unabhängige Zählerwerte.

Was die Fiber neben diesen beiden Feldern enthält

type und memoizedState reichen aus, um das Problem mit dem Zustand zu lösen, doch eine echte Fiber enthält weitaus mehr Informationen. Sie speichert einen Verweis auf den DOM-Node, den sie erzeugt hat, Zeiger auf ihren Elternknoten, das erste Kind sowie den nächsten Geschwisterknoten, damit React den Baum durchlaufen kann, sowie die vorherigen Props zur Vergleich mit den neu eingehenden. Diese Felder bestimmen, wie React herausfindet, was sich geändert hat, und wie es einen ganzen Komponentenbaum durchläuft – was ein eigenständiges Problem ist im Vergleich dazu, wo sich ein einzelner Wert befindet.

Was jetzt klar definiert werden kann, ist die Trennlinie zwischen den beiden Objekten, denn deren Verschmelzung ist die Ursache vieler Verwirrungen bezüglich der Darstellung:

Element                           Fiber
--------                          -----
Created by React.createElement    Created internally by React
New object every render           Same object, updated in place
Discarded right after             Persists for the component's
  React reads it                    entire mounted lifetime
Holds no history                  Holds memoizedState, the real
                                     remembered value across renders
Describes what should exist       Is the thing that actually exists,
                                     with real memory attached to it

Eine kompakte Art, sich das zu merken: Ein Element ist eine Anfrage an React, während eine Fiber die eigene Aufzeichnung von dem darstellt, was derzeit existiert. Jede Renderung erzeugt eine neue Gruppe billiger, memorieloser Elemente und übergibt sie an den Fiber-Tree, der bereits aus der vorherigen Renderung vorhanden ist und alles enthält, was erhalten bleiben muss. Eine Seite beschreibt, die andere erinnert sich.

Ein offenes Problem: Fibers kommen in Paaren

Die Aussage, dass eine Fiber „vor Ort aktualisiert“ wird, ist eine nützliche erste Annäherung, aber sie ist nicht die ganze Wahrheit. React behält tatsächlich zwei Versionen jeder Fiber bei – eine, die dem aktuellen Anzeigebild entspricht, und eine, die für die nächste Aktualisierung vorbereitet wird – und wechselt zwischen ihnen. Diese Anordnung ermöglicht es React, an einer Aktualisierung zu arbeiten, ohne die sichtbare Benutzeroberfläche zu stören, und verdient eine eigene Erklärung.

Kernpunkte

  • Die lokalen Variablen einer Funktionskomponente werden bei jedem Aufruf neu erstellt, sodass die Funktion selbst keinen Zustand speichern kann.
  • JSX wird zu Aufrufen kompiliert, die Elemente erzeugen: einfache, verwendbare Beschreibungen, die bei jeder Renderung neu erstellt werden.
  • Fibers sind einfache Objekte, die solange bestehen bleiben, wie eine Komponente geladen ist; type gibt an, welche Komponente eine Fiber verfolgt.
  • useState liest und schreibt Werte über den memoizedState der Fiber, der in Wirklichkeit auf eine Liste von Hook-Objekten verweist, die nach Reihenfolge der Aufrufe abgestimmt sind.
  • Der an useState übergebene Anfangswert ist nur beim Laden relevant; danach ist die Fiber die Quelle der Wahrheit.

Zusätzliche Literatur