Kapitel 4. Innere Versöhnung
Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com
Um React wirklich gut zu beherrschen, müssen wir verstehen , was die Funktionen tun. Bis jetzt haben wir JSX und React.createElement
verstanden. Wir haben auch das virtuelle DOM einigermaßen detailliert verstanden. In diesem Kapitel wollen wir die praktischen Anwendungen vonReact erkunden und verstehen, was ReactDOM.createRoot(element).render()
tut. Wir werden insbesondere untersuchen , wie React sein virtuelles DOM aufbaut und dann das reale DOM durch einen Prozess aktualisiert, der sichAbgleich nennt .
Die Versöhnung verstehen
Um es kurz zu machen: Das virtuelle DOM von React ist ein Entwurf unseres gewünschten UI-Zustands. React nimmt diesen Entwurf und setzt ihn durch einen Prozess namensReconciliation in einer bestimmten Host-Umgebung um; normalerweise ein Webbrowser, aber möglicherweise auch andere Umgebungen wie Shells, native Plattformen wie iOS und Android und mehr.
Betrachte den folgenden Codeschnipsel:
import
{
useState
}
from
"react"
;
const
App
=
()
=>
{
const
[
count
,
setCount
]
=
useState
(
0
);
return
(
<
main
>
<
div
>
<
h1
>
Hello
,
world
!<
/h1>
<
span
>
Count
:
{
count
}
<
/span>
<
button
onClick
=
{()
=>
setCount
(
count
+
1
)}
>
Increment
<
/button>
<
/div>
<
/main>
);
};
Dieser Codeschnipsel enthält eine deklarative Beschreibung dessen, was unser UI-Zustand sein soll: ein Baum von Elementen. Sowohl unsere Teammitglieder als auch React können dies lesen und verstehen, dass wir versuchen, eine Zähler-App mit einem Inkrement-Button zu erstellen, der den Zähler inkrementiert. Um die Versöhnung zu verstehen, müssen wir wissen, was React tut, wenn es mit einer Komponente wie dieser konfrontiert wird.
Zuerst wird das JSX zu einem Baum aus React-Elementen. Das haben wir bereits in Kapitel 3 gesehen. Wenn die App
Komponente aufgerufen wird, gibt sie ein React-Element zurück, dessen Kinder weitere React-Elemente sind. React-Elemente sind unveränderlich (für uns) und stellen den gewünschten Zustand der Benutzeroberfläche dar. Sie sind nicht der tatsächliche Zustand der Benutzeroberfläche. React-Elemente werden durchReact.createElement
oder das JSX-Symbol <
erstellt, so dass dies in umgesetzt werden würde:
const
App
=
()
=>
{
const
[
count
,
setCount
]
=
useState
(
0
);
return
React
.
createElement
(
"main"
,
null
,
React
.
createElement
(
"div"
,
null
,
React
.
createElement
(
"h1"
,
null
,
"Hello, world!"
),
React
.
createElement
(
"span"
,
null
,
"Count: "
,
count
),
React
.
createElement
(
"button"
,
{
onClick
:
()
=>
setCount
(
count
+
1
)
},
"Increment"
)
)
);
};
So erhalten wir einen Baum von erstellten React-Elementen, der etwa so aussieht:
{
type
:
"main"
,
props
:
{
children
:
{
type
:
"div"
,
props
:
{
children
:
[
{
type
:
"h1"
,
props
:
{
children
:
"Hello, world!"
,
},
},
{
type
:
"span"
,
props
:
{
children
:
[
"Count: "
,
count
],
},
},
{
type
:
"button"
,
props
:
{
onClick
:
()
=>
setCount
(
count
+
1
),
children
:
"Increment"
,
},
},
],
},
},
},
}
Dieser Ausschnitt stellt das virtuelle DOM dar, das von unserer KomponenteCounter
stammt. Da dies das erste Rendering ist, wird dieser Baum nun mit minimalen Aufrufen der imperativen DOM-APIs an den Browser übergeben. Wie stellt React sicher, dass nur minimale Aufrufe an imperative DOM-APIs erfolgen? Indem vDOM-Updates zu einem echten DOM-Update zusammengefasst werden und das DOM so wenig wie möglich berührt wird, wie in den vorherigen Kapiteln beschrieben. Um das Batching besser zu verstehen, wollen wir uns das genauer ansehen.
Batching
In Kapitel 3 haben wir Dokumentfragmente in Browsern als Teil der eingebauten DOM-APIs besprochen: leichtgewichtige Container, die Sammlungen von DOM-Knoten enthalten, die wie ein temporärer Aufenthaltsbereich wirken, in dem du mehrere Änderungen vornehmen kannst, ohne das Haupt-DOM zu beeinflussen, bis du schließlich das Dokumentfragment an das DOM anhängst, was einen einzigen Reflow und eine Neuzeichnung auslöst.
In ähnlicher Weise bündelt React Aktualisierungen des realen DOM während des Abgleichs und kombiniert mehrere vDOM Aktualisierungen zu einer einzigen DOM Aktualisierung. Dadurch wird die Anzahl der Aktualisierungen des realen DOMs reduziert und die Leistung von Webanwendungen verbessert.
Um das zu verstehen, lass uns eine Komponente betrachten, die ihren Zustand mehrmals kurz hintereinander aktualisiert:
function
Example
()
{
const
[
count
,
setCount
]
=
useState
(
0
);
const
handleClick
=
()
=>
{
setCount
((
prevCount
)
=>
prevCount
+
1
);
setCount
((
prevCount
)
=>
prevCount
+
1
);
setCount
((
prevCount
)
=>
prevCount
+
1
);
};
return
(
<
div
>
<
p
>
Count
:
{
count
}</
p
>
<
button
onClick
=
{
handleClick
}>
Increment
</
button
>
</
div
>
);
}
In diesem Beispiel ruft die Funktion handleClick
dreimal kurz hintereinander setCount
auf. Ohne Batching würde React das reale DOM dreimal aktualisieren, obwohl sich der Wert von count
nur einmal geändert hat. Das wäre verschwenderisch und langsam.
Da React jedoch Aktualisierungen stapelweise vornimmt, wird das DOM mit count + 3
nur einmal aktualisiert, anstatt dreimal mit count + 1
.
Um die effizienteste Batch-Aktualisierung des DOMs zu berechnen, erstellt React einen neuen vDOM-Baum als Fork des aktuellen vDOM-Baums mit den aktualisierten Werten, wobei count
3
ist. Dieser Baum muss mit den aktuellen Werten im Browserabgeglichen werden, wodurch0
zu 3
wird. React berechnet dann, dass nur eine Aktualisierung des DOM mit dem neuen vDOM-Wert 3
erforderlich ist, anstatt das DOM dreimal manuell zu aktualisieren. So sieht das Batching aus, und es ist ein Teil des umfassenderen Themas, mit dem wir uns gleich beschäftigen werden: Reconciliation, also der Prozess, bei dem der nächste erwartete DOM-Zustand mit dem aktuellen DOM abgeglichen wird.
Bevor wir verstehen, was das moderne React unter der Haube macht, wollen wir uns ansehen, wie React vor Version 16 den Abgleich mit dem alten "Stack"-Abstimmer durchgeführt hat. Dies wird uns helfen, die Notwendigkeit des heute so beliebten Fiber Reconcilers zu verstehen.
Hinweis
An dieser Stelle sei erwähnt, dass es sich bei allen Themen, die wir besprechen werden, um Implementierungsdetails in React handelt, die sich im Laufe der Zeit ändern können und wahrscheinlich auch werden. Wir isolieren hier die Mechanismen, wie React funktioniert, von der tatsächlichen praktischen Nutzung von React. Das Ziel ist, dass wir durch das Verständnis der internen Mechanismen von React besser verstehen, wie wir React effektiv in Anwendungen einsetzen können.
Stand der Technik
Bisher hat React eine Stack-Datenstruktur für das Rendering verwendet. Um sicherzugehen, dass wir auf der gleichen Seite stehen, wollen wir kurz die Datenstruktur des Stacks besprechen.
Stack Reconciler (Legacy)
In der Informatik ist ein Stack eine lineare Datenstruktur, die dem LIFO-Prinzip (last in, first out) folgt. Das bedeutet, dass das letzte Element, das dem Stapel hinzugefügt wird, auch das erste ist, das wieder entfernt wird. Ein Stapel hat zwei grundlegende Operationen, push und pop, mit denen Elemente oben auf dem Stapel hinzugefügt bzw. entfernt werden können.
Ein Stapel kann als eine Sammlung von Elementen dargestellt werden, die vertikal angeordnet sind, wobei das oberste Element das zuletzt hinzugefügte ist. Hier ist eine ASCII-Illustration eines Stapels mit drei Elementen:
-----
|
3
|
|
___
|
|
2
|
|
___
|
|
1
|
|
___
|
In diesem Beispiel ist das zuletzt hinzugefügte Element 3
, das ganz oben auf dem Stapel liegt. Das Element 1
, das als erstes hinzugefügt wurde, befindet sich am unteren Ende des Stapels.
In diesem Stapel wird durch die Push-Operation ein Element an die Spitze des Stapels hinzugefügt. Im Code kann dies in JavaScript mithilfe eines Arrays und der Methode push
wie folgt ausgeführt werden:
const
stack
=
[];
stack
.
push
(
1
);
// stack is now [1]
stack
.
push
(
2
);
// stack is now [1, 2]
stack
.
push
(
3
);
// stack is now [1, 2, 3]
Die Pop-Operation entfernt das oberste Element vom Stapel. Im Code kann dies in JavaScript mit einem Array und der Methode pop
ausgeführt werden, etwa so:
const
stack
=
[
1
,
2
,
3
];
const
top
=
stack
.
pop
();
// top is now 3, and stack is now [1, 2]
In diesem Beispiel entfernt die Methode pop
das oberste Element (3
) vom Stapel und gibt es zurück. Das Stack-Array enthält nun die restlichen Elemente (1
und 2
).
Reacts ursprünglicher Reconciler war ein stapelbasierter Algorithmus, mit dem der alte und der neue virtuelle Baum verglichen und das DOM entsprechend aktualisiert wurde. Während der Stack-Reconciler in einfachen Fällen gut funktionierte, stellte er eine Reihe von Herausforderungen dar, wenn die Anwendungen größer und komplexer wurden.
Schauen wir uns kurz an, warum das der Fall ist. Dazu nehmen wir ein Beispiel, bei dem wir eine Liste von Aktualisierungen vornehmen müssen:
-
Eine nicht notwendige, rechenintensive Komponente verbraucht CPU undrendert.
-
Ein Benutzer tippt in ein
input
Element. -
Button
wird aktiviert, wenn die Eingabe gültig ist. -
Eine
Form
Komponente enthält den Status, so dass er wiederhergestellt wird.
Im Code würden wir das so ausdrücken:
import
React
,
{
useReducer
}
from
"react"
;
const
initialState
=
{
text
:
""
,
isValid
:
false
};
function
Form
()
{
const
[
state
,
dispatch
]
=
useReducer
(
reducer
,
initialState
);
const
handleChange
=
(
e
)
=>
{
dispatch
({
type
:
"handleInput"
,
payload
:
e
.
target
.
value
});
};
return
(
<
div
>
<
ExpensiveComponent
/>
<
input
value
=
{
state
.
text
}
onChange
=
{
handleChange
}
/>
<
Button
disabled
=
{
!
state
.
isValid
}>
Submit
</
Button
>
</
div
>
);
}
function
reducer
(
state
,
action
)
{
switch
(
action
.
type
)
{
case
"handleInput"
:
return
{
text
:
action
.
payload
,
isValid
:
action
.
payload
.
length
>
0
,
};
default
:
throw
new
Error
();
}
}
In diesem Fall würde der Stack Reconciler die Aktualisierungen nacheinander rendern, ohne die Arbeit unterbrechen oder verschieben zu können. Wenn die rechenintensive Komponente das Rendering blockiert, erscheinen die Benutzereingaben mit einer spürbaren Verzögerung auf dem Bildschirm. Das führt zu einer schlechten Benutzererfahrung, da das Textfeld nicht reagieren würde. Stattdessen wäre es viel angenehmer, die Benutzereingabe als eine Aktualisierung mit höherer Priorität zu erkennen als das Rendern der unwichtigen, teuren Komponente und den Bildschirm entsprechend zu aktualisieren und das Rendern der rechenintensiven Komponente aufzuschieben.
Es muss möglich sein, die laufenden Rendering-Arbeiten zu beenden, wenn sie durch Rendering-Arbeiten mit höherer Priorität unterbrochen werden, z. B. durch Benutzereingaben. Um das zu erreichen, muss React bestimmten Arten von Rendering-Vorgängen Vorrang vor anderen geben.
Der Stack Reconciler hat Aktualisierungen nicht nach Priorität geordnet , was bedeutet, dass weniger wichtige Aktualisierungen wichtigere Aktualisierungen blockieren können. So konnte z. B. eine Aktualisierung eines Tooltips mit niedriger Priorität eine Aktualisierung einer Texteingabe mit hoher Priorität blockieren. Die Aktualisierungen des virtuellen Baums wurden in der Reihenfolge ausgeführt, in der sie empfangen wurden.
In einer React-Anwendung können Aktualisierungen des virtuellen Baums unterschiedlich wichtig sein. So kann zum Beispiel die Aktualisierung einer Formulareingabe wichtiger sein als die Aktualisierung eines Indikators, der die Anzahl der Likes für einen Beitrag anzeigt, weil der Nutzer direkt mit der Eingabe interagiert und erwartet, dass sie responsiv ist.
Im Stapelabgleich wurden die Aktualisierungen in der Reihenfolge ihres Eingangs ausgeführt, was bedeutete, dass weniger wichtige Aktualisierungen wichtigere Aktualisierungen blockieren konnten. Wenn zum Beispiel eine Aktualisierung des Like-Counters vor einer Aktualisierung der Formulareingabe empfangen wurde, wurde die Aktualisierung des Like-Counters zuerst ausgeführt und konnte die Aktualisierung der Formulareingabe blockieren.
Wenn die Aktualisierung des Like-Counters lange dauert (z. B. weil sie eine teure Berechnung durchführt), kann dies zu einer spürbaren Verzögerung oder einem Ruckeln in der Benutzeroberfläche führen, vor allem wenn der Benutzer während der Aktualisierung mit der Anwendung interagiert.
Eine weitere Herausforderung beim Stack Reconciler war, dass er es nicht zuließ, dass Aktualisierungen unterbrochen oder abgebrochen werden konnten. Das bedeutet, dass der Stack Reconciler zwar ein Gefühl für die Priorität von Aktualisierungen hatte, aber nicht garantieren konnte, dass er mit verschiedenen Prioritäten arbeiten konnte, indem er unwichtige Arbeiten abbrach, wenn eine Aktualisierung mit hoher Prioritätgeplant war.
In jeder Webanwendung sind nicht alle Aktualisierungen gleich: Eine zufällig auftauchende unerwartete Benachrichtigung ist nicht so wichtig wie die Reaktion auf meinen Klick auf eine Schaltfläche, denn letztere ist eine bewusste Handlung, die eine sofortige Reaktion erfordert, während erstere nicht einmal erwartet wird und vielleicht nicht einmal willkommen ist.
Im Stack Reconciler konnten Aktualisierungen nicht unterbrochen oder abgebrochen werden, was bedeutete, dass unnötige Aktualisierungen, wie z. B. das Anzeigen eines Toasts, manchmal auf Kosten von Benutzerinteraktionen durchgeführt wurden. Dies konnte dazu führen, dass unnötige Arbeit am virtuellen Baum und am DOM geleistet wurde, was sich negativ auf die Leistung derAnwendung auswirkte.
Der Stack Reconciler stand vor einer Reihe von Herausforderungen, als die Anwendungen immer größer und komplexer wurden. Die größten Herausforderungen waren Jank und die langsame Reaktion der Benutzeroberfläche. Um diese Herausforderungen zu bewältigen, entwickelte das React-Team einen neuen Reconciler, den Fiber Reconciler, der auf einer anderen Datenstruktur, dem Fiber Tree, basiert. Diese Datenstruktur werden wir im nächsten Abschnitt näher betrachten.
Der Fiber Reconciler
Der Fiber Reconciler verwendet eine andere Datenstruktur namens "Fiber", die eine einzelne Arbeitseinheit für den Reconciler darstellt. Fibers werden aus React-Elementen erstellt, die wir inKapitel 3 behandelt haben. Der Hauptunterschied besteht darin, dass sie zustandsbehaftet und langlebig sind, während React-Elemente flüchtig und zustandslos sind.
Mark Erikson, der Redux-Maintainer und prominente React-Experte, beschreibt Fibers als "Reacts interne Datenstruktur, die den aktuellen Komponentenbaum zu einem bestimmten Zeitpunkt darstellt". Das ist in der Tat eine gute Art, über Fibers nachzudenken, und es passt zu Mark, der zum Zeitpunkt der Erstellung dieses Artikels hauptberuflich mit Replay am Debugging von React-Apps auf Zeitreise arbeitet: ein Tool, mit dem du den Zustand deiner App zum Debuggen zurückspulen und wiedergeben kannst. Falls du es noch nicht getan hast, findest du auf Replay.io weitere Informationen.
Ähnlich wie das vDOM ein Baum aus Elementen ist, verwendet React bei der Abstimmung einen Faserbaum, der, wie der Name schon sagt, ein Baum aus Fasern ist, der direkt dem vDOM nachempfunden ist.
Die Faser als Datenstruktur
Die Fiber Datenstruktur in React ist eine Schlüsselkomponente des Fiber Reconcilers. Mit dem Fiber Reconciler können Aktualisierungen priorisiert und gleichzeitig ausgeführt werden, was die Leistung und Reaktionsfähigkeit von React-Anwendungen verbessert. Schauen wir uns die Fiber Datenstruktur genauer an.
Im Kern ist die Datenstruktur Fiber eine Darstellung einer Komponenteninstanz und ihres Zustands in einer React-Anwendung. Wie bereits erwähnt, ist die Datenstruktur Fiber als veränderbare Instanz konzipiert und kann während des Abgleichs nach Bedarf aktualisiert und neu angeordnet werden.
Jede Instanz eines Fiber Knotens enthält Informationen über die Komponente, die er repräsentiert, einschließlich ihrer Eigenschaften, ihres Zustands und ihrer untergeordneten Komponenten. Der Fiber-Knoten enthält außerdem Informationen über seine Position im Komponentenbaum sowie Metadaten, die vom Fiber-Reconciler verwendet werden, um Aktualisierungen zu priorisieren und auszuführen.
Hier ist ein Beispiel für einen einfachen Fiber-Knoten:
{
tag
:
3
,
// 3 = ClassComponent
type
:
App
,
key
:
null
,
ref
:
null
,
props
:
{
name
:
"Tejas"
,
age
:
30
},
stateNode
:
AppInstance
,
return
:
FiberParent
,
child
:
FiberChild
,
sibling
:
FiberSibling
,
index
:
0
,
//...
}
In diesem Beispiel haben wir einen Faserknoten, der eine ClassComponent
namens App
darstellt. Der Faserknoten enthält Informationen über die Komponente:
tag
-
In diesem Fall ist es
3
, die React verwendet, um Klassenkomponenten zu identifizieren. Jeder Komponententyp (Klassenkomponenten, Funktionskomponenten, Suspense- und Fehlergrenzen, Fragmente usw.) hat seine eigene numerische ID als Fibers. type
-
App
bezieht sich auf die Funktion oder Klassenkomponente, die diese Faser repräsentiert. props
-
(
{name: "Tejas", age: 30}
) stellen die Eingangsrequisiten für die Komponente oder die Eingangsargumente für die Funktion dar. stateNode
-
Die Instanz der Komponente
App
, die diese Faser darstellt.Ihre Position im Komponentenbaum:
return
child
,sibling
undindex
geben dem Fiber reconciler die Möglichkeit, den Baum zu durchwandern und die Eltern, Kinder, Geschwister und den Index der Fiber zu identifizieren.
Beim Faserabgleich wird der aktuelle Faserbaum mit dem nächsten Faserbaum verglichen und es wird ermittelt, welche Knoten aktualisiert, hinzugefügt oder entfernt werden müssen.
Während des Abstimmungsprozesses erstellt der Fiber reconciler einen Fiber-Knoten für jedes React-Element im virtuellen DOM. Es gibt eine Funktion namens createFiberFromTypeAndProps
, die dies tut. Eine andere Art, "type und props" zu sagen, ist natürlich, sie als React-Elemente zu bezeichnen. Wie wir uns erinnern, ist ein React-Element dies: type und props:
{
type
:
"div"
,
props
:
{
className
:
"container"
}
}
Diese Funktion gibt eine aus Elementen abgeleitete Fiber zurück. Sobald die Faserknoten erstellt wurden, verwendet der Faserabgleicher eine Arbeitsschleife, um die Benutzeroberfläche zu aktualisieren. Die Arbeitsschleife beginnt am Wurzelknoten der Faser und arbeitet sich im Komponentenbaum nach unten, wobei jeder Faserknoten als "schmutzig" markiert wird, wenn er aktualisiert werden muss. Am Ende angekommen, geht sie wieder nach oben und erstellt einen neuen DOM-Baum im Speicher, der vom Browser losgelöst ist und schließlich auf den Bildschirm übertragen wird (flushed). Dies wird durch zwei Funktionen dargestellt: beginWork
wandert abwärts und markiert Komponenten als "zu aktualisieren" undcompleteWork
läuft wieder nach oben und baut einen Baum aus echten DOM-Elementen auf, der vom Browser getrennt ist. Dieser Rendering-Prozess außerhalb des Bildschirms kann jederzeit unterbrochen und verworfen werden, da der Nutzer ihn nicht sieht.
Die Fiber-Architektur ist von einem Konzept inspiriert, das in der Spielwelt "Double Buffering" genannt wird. Dabei wird der nächste Bildschirm außerhalb des Bildschirms vorbereitet und dann auf den aktuellen Bildschirm "gespült". Um die Fiber-Architektur besser zu verstehen, wollen wir dieses Konzept etwas genauer erklären, bevor wir weitergehen.
Doppelte Pufferung
Double Buffering ist eine Technik, die in der Computergrafik und Videobearbeitung eingesetzt wird, um Flimmern zu reduzieren und die wahrgenommene Leistung zu verbessern. Bei dieser Technik werden zwei Puffer (oder Speicherbereiche) für die Speicherung von Bildern oder Einzelbildern erstellt und in regelmäßigen Abständen zwischen ihnen umgeschaltet, um das endgültige Bild oder Video anzuzeigen.
So funktioniert die doppelte Pufferung in der Praxis:
-
Der erste Puffer wird mit dem ersten Bild oder Frame gefüllt.
-
Während der erste Puffer angezeigt wird, wird der zweite Puffer mit neuen Daten oder Bildern aktualisiert.
-
Wenn der zweite Puffer fertig ist, wird er mit dem ersten Puffer vertauscht und auf dem Bildschirm angezeigt.
-
Der Prozess geht weiter, wobei der erste und der zweite Puffer in regelmäßigen Abständen gewechselt werden, um das endgültige Bild oder Video anzuzeigen.
Durch die doppelte Pufferung können Flimmern und andere visuelle Artefakte reduziert werden, da das endgültige Bild oder Video ohne Unterbrechungen oder Verzögerungen angezeigt wird.
Die Fiber reconciliation ähnelt dem Double Buffering, d.h. bei Aktualisierungen wird der aktuelle Fiber Tree geforkt und aktualisiert, um den neuen Zustand einer bestimmten Benutzeroberfläche wiederzugeben. Dieser Vorgang wird genannt. Wenn der alternative Baum fertig ist und genau den Zustand widerspiegelt, den der Benutzer erwartet, wird er mit dem aktuellen Baum ausgetauscht, ähnlich wie beim Double Buffering die Videopuffer ausgetauscht werden. Dies wird alsCommit-Phase des Abgleichs oder als Commit bezeichnet.
Durch die Verwendung eines Work-in-Progress-Baums bietet der Fiber Reconciler eine Reihe von Vorteilen:
-
So können unnötige Aktualisierungen des echten DOM vermieden werden, was die Leistung verbessert und das Flackern reduziert.
-
Es kann den neuen Zustand einer Benutzeroberfläche außerhalb des Bildschirms berechnen und ihn wegwerfen, wenn eine neue Aktualisierung mit höherer Priorität durchgeführt werden muss.
-
Da die Abstimmung außerhalb des Bildschirms stattfindet, kann sie sogar angehalten und fortgesetzt werden, ohne dass das, was der Nutzer gerade sieht, durcheinander kommt.
Mit dem Fiber reconciler werden zwei Bäume aus einem benutzerdefinierten Baum vonJSX-Elementen abgeleitet: ein Baum mit "aktuellen" Fasern und ein anderer Baum mit in Arbeit befindlichen Fasern. Schauen wir uns diese Bäume ein wenig genauer an.
Fiber Reconciliation
Der Faserabgleich erfolgt in zwei Phasen: der Rendering-Phase und der Commit-Phase. Dieser Zwei-Phasen-Ansatz, der in Abbildung 4-1 dargestellt ist, ermöglicht es React, Rendering-Arbeiten auszuführen, die jederzeit beendet werden können, bevor sie in das DOM übertragen werden und den Nutzern ein neuer Zustand angezeigt wird: Das Rendering wird dadurch unterbrechbar. Was das Rendering unterbrechbar macht, ist die Heuristik des Zeitplannungsprogramms von React, das die Ausführung alle 5 ms an den Hauptthread zurückgibt, was selbst auf Geräten mit 120 Bildern pro Sekunde weniger als ein Einzelbild ist.
In Kapitel 7 werden wir mehr über das Zeitplannungsprogramm erfahren, wenn wir uns mit den nebenläufigen Funktionen von React beschäftigen. Aber jetzt lass uns erst einmal diese Phasen derAbstimmung durchgehen.
Die Rendering-Phase
Die Rendering-Phase startet, wenn imcurrent
Baum ein Ereignis auftritt, das den Zustand verändert. React führt die Änderungen außerhalb des Bildschirmsim alternate
Baum durch, indem es rekursiv durch jede Faser geht und Flags setzt, die anzeigen, dass Aktualisierungen anstehen (siehe Abbildung 4-2). Wie wir bereits angedeutet haben, geschieht dies in einer internen Funktion von React namens beginWork
.
beginWork
beginWork
ist verantwortlich für das Setzen von Flags an den Fiber-Knoten im Work-in-Progress-Baum, die angeben, ob sie aktualisiert werden sollen oder nicht. Er setzt eine Reihe von Flags und geht dann rekursiv zum nächsten Faserknoten und macht dasselbe, bis er das Ende des Baums erreicht. Wenn er fertig ist, rufen wir completeWork
auf den Faserknoten auf und gehen wieder nach oben.
Die Unterschrift von beginWork
lautet wie folgt:
function
beginWork
(
current
:
Fiber
|
null
,
workInProgress
:
Fiber
,
renderLanes
:
Lanes
)
:
Fiber
|
null
;
Mehr zu completeWork
später. Jetzt wollen wir erst einmal beginWork
kennenlernen. Seine Signatur enthält die folgenden Argumente:
current
-
Eine Referenz auf den Fiber-Knoten im aktuellen Baum, der dem zu aktualisierenden Work-in-Progress-Knoten entspricht. Er wird verwendet, um festzustellen, was sich zwischen der vorherigen und der neuen Version des Baums geändert hat und was aktualisiert werden muss. Er wird nieverändert und dient nur zum Vergleich.
workInProgress
-
Der zu aktualisierende Faserknoten im Baum "work-in-progress". Dies ist der Knoten, der als "schmutzig" markiert wird, wenn er aktualisiert und von der Funktion zurückgegeben wird.
renderLanes
-
Render Lanes ist ein neues Konzept in Reacts Fiber reconciler, das das ältere
renderExpirationTime
ersetzt. Es ist etwas komplexer als das alterenderExpirationTime
Konzept, aber es ermöglicht React, Aktualisierungen besser zu priorisieren und den Aktualisierungsprozess effizienter zu gestalten. DarenderExpirationTime
veraltet ist, werden wir uns in diesem Kapitel aufrenderLanes
konzentrieren.Es handelt sich im Wesentlichen um eine Bitmaske, die "Lanes" darstellt, in denen eine Aktualisierung verarbeitet wird. Lanes sind eine Möglichkeit, Aktualisierungen anhand ihrer Priorität und anderer Faktoren zu kategorisieren. Wenn eine Änderung an einer React-Komponente vorgenommen wird, wird ihr auf der Grundlage ihrer Priorität und anderer Merkmale eine Lane zugewiesen. Je höher die Priorität der Änderung ist, desto höher ist die Lane, der sie zugewiesen wird.
Der Wert
renderLanes
wird an die FunktionbeginWork
weitergegeben, um sicherzustellen, dass die Aktualisierungen in der richtigen Reihenfolge verarbeitet werden. Aktualisierungen, die den Lanes mit höherer Priorität zugewiesen sind, werden vor den Aktualisierungen verarbeitet, die den Lanes mit niedrigerer Priorität zugewiesen sind. Dadurch wird sichergestellt, dass Aktualisierungen mit hoher Priorität, wie z. B. Aktualisierungen, die sich auf die Benutzerinteraktion oder die Zugänglichkeit auswirken, so schnell wie möglich verarbeitet werden.Neben der Priorisierung von Aktualisierungen hilft
renderLanes
React auch, die Gleichzeitigkeit besser zu verwalten. React verwendet eine Technik , die "Time Slicing" genannt wird, um langlaufende Aktualisierungen in kleinere, besser handhabbare Teile aufzuteilen.renderLanes
spielt bei diesem Prozess eine Schlüsselrolle, da es React ermöglicht, zu bestimmen, welche Aktualisierungen zuerst verarbeitet werden sollten und welche auf einen späteren Zeitpunkt verschoben werden können.Nach Abschluss der Rendering-Phase wird die Funktion
getLanesToRetrySynchronouslyOnError
aufgerufen, um festzustellen, ob während der Rendering-Phase aufgeschobene Aktualisierungen erstellt wurden. Wenn es aufgeschobene Aktualisierungen gibt, startet die FunktionupdateComponent
eine neue Arbeitsschleife, um sie zu bearbeiten. Dabei werdenbeginWork
undgetNextLanes
verwendet, um die Aktualisierungen zu verarbeiten und sie nach ihren Fahrspuren zu priorisieren.In Kapitel 7, dem nächsten Kapitel über Gleichzeitigkeit, werden wir viel tiefer in Rendering Lanes eintauchen. Für den Moment folgen wir weiter dem Fiber reconciliation flow.
completeWork
Die Funktion completeWork
wendet Aktualisierungen auf den laufenden Fiber-Knoten an und konstruiert einen neuen echten DOM-Baum, der den aktualisierten Zustand der Anwendung darstellt. Sie konstruiert diesen Baum losgelöst vom DOM außerhalb der Sichtbarkeitsebene des Browsers.
Wenn es sich bei der Host-Umgebung um einen Browser handelt, bedeutet dies, dass du Dinge wiedocument.createElement
oder newElement.appendChild
tun musst. Bedenke, dass dieser Baum von Elementen noch nicht mit dem Dokument im Browser verbunden ist: React erstellt gerade die nächste Version der Benutzeroberfläche außerhalb des Bildschirms. Dadurch, dass diese Arbeit außerhalb des Bildschirms stattfindet, kann sie unterbrochen werden: Der nächste Zustand, den React berechnet, ist noch nicht auf den Bildschirm gemalt, so dass er weggeworfen werden kann, wenn eine Aktualisierung mit höherer Priorität geplant wird. Genau das ist der Sinn des Fiberreconcilers.
Die Unterschrift von completeWork
lautet wie folgt:
function
completeWork
(
current
:
Fiber
|
null
,
workInProgress
:
Fiber
,
renderLanes
:
Lanes
)
:
Fiber
|
null
;
Hier ist die Unterschrift die gleiche wie bei beginWork
.
Die Funktion completeWork
ist eng mit der Funktion beginWork
verwandt. Während beginWork
für das Setzen von Flags über den "should update"-Status eines Fiber Nodes verantwortlich ist, ist für den Aufbau eines neuen Baums zuständig,completeWork
für den Aufbau eines neuen Baums verantwortlich, der an die Host-Umgebung übergeben wird. Wenn completeWork
den Gipfel erreicht und den neuen DOM-Baum aufgebaut hat, sagen wir, dass "die Rendering-Phase abgeschlossen ist". Jetzt geht React zur Commit-Phase über.
Die Commit-Phase
Die Commit-Phase (siehe Abbildung 4-3) ist dafür verantwortlich, das tatsächliche DOM mit den Änderungen zu aktualisieren, die während der Rendering-Phase am virtuellen DOM vorgenommen wurden. Während der Commit-Phase wird der neue virtuelle DOM-Baum in die Host-Umgebung übertragen und der Work-in-Progress-Baum wird durch den aktuellen Baum ersetzt. In dieser Phase werden auch alle Effekte ausgeführt. Die Commit-Phase ist in zwei Teile unterteilt: die Mutationsphase und die Layout-Phase.
Die Mutationsphase
Die Mutationsphase ist der erste Teil der Commit-Phase. Sie ist dafür verantwortlich, das tatsächliche DOM mit den Änderungen zu aktualisieren, die am virtuellen DOM vorgenommen wurden. In dieser Phase identifiziert React die Aktualisierungen, die vorgenommen werden müssen, und ruft eine spezielle Funktion namenscommitMutationEffects
auf. Diese Funktion wendet die Aktualisierungen, die während der Rendering-Phase an den Faserknoten im alternativen Baum vorgenommen wurden, auf das tatsächliche DOM an.
Hier ist ein vollständiges Pseudocode-Beispiel, wie commitMutationEffects
implementiert werden könnte:
function
commitMutationEffects
(
Fiber
)
{
switch
(
Fiber
.
tag
)
{
case
HostComponent
:
{
// Update DOM node with new props and/or children
break
;
}
case
HostText
:
{
// Update text content of DOM node
break
;
}
case
ClassComponent
:
{
// Call lifecycle methods like componentDidMount and componentDidUpdate
break
;
}
// ... other cases for different types of nodes
}
}
Während der Mutationsphase ruft React auch andere spezielle Funktionen auf, wie commitUnmount
und commitDeletion
, um Knoten aus dem DOM zu entfernen, die nicht mehr benötigt werden.
Die Layout-Phase
Die Layout-Phase ist der zweite Teil der Commit-Phase. Sie ist für die Berechnung des neuen Layouts der aktualisierten Knoten im DOM zuständig. Während dieser Phase ruft React eine spezielle Funktion namenscommitLayoutEffects
auf. Diese Funktion errechnet das neue Layout der aktualisierten Knoten im DOM.
Wie commitMutationEffects
ist auch commitLayoutEffects
eine massive Switch-Anweisung, die je nach Art des zu aktualisierenden Knotens verschiedene Funktionen aufruft.
Sobald die Layout-Phase abgeschlossen ist, hat React das tatsächliche DOM erfolgreich aktualisiert, um die Änderungen widerzuspiegeln, die während der Rendering-Phase am virtuellen DOM vorgenommen wurden.
Durch die Aufteilung der Commit-Phase in zwei Teile (Mutation und Layout) ist React in der Lage, Aktualisierungen im DOM auf effiziente Weise vorzunehmen. Die Commit-Phase arbeitet mit anderen wichtigen Funktionen des Reconcilers zusammen und trägt dazu bei, dass React-Anwendungen schnell, reaktionsschnell und zuverlässig sind, auch wenn sie komplexer werden und größere Datenmengen verarbeiten.
Effekte
Während der Commit-Phase des React-Abgleichsprozesses werden die Seiteneffekteje nach Art des Effekts in einer bestimmten Reihenfolge ausgeführt. Es gibt verschiedene Arten von Effekten, die während der Commit-Phase auftreten können, darunter:
- Platzierungseffekte
-
Diese Effekte treten auf, wenn eine neue Komponente zum DOM hinzugefügt wird. Wenn zum Beispiel eine neue Schaltfläche zu einem Formular hinzugefügt wird, tritt ein Platzierungseffekt auf, um die Schaltfläche zum DOM hinzuzufügen.
- Effekte aktualisieren
-
Diese Effekte treten auf, wenn eine Komponente mit neuen Requisiten oder Zuständen aktualisiert wird. Wenn sich zum Beispiel der Text einer Schaltfläche ändert, wird ein Aktualisierungseffekt ausgelöst, um den Text im DOM zu aktualisieren.
- Auswirkungen der Löschung
-
Diese Effekte treten auf, wenn eine Komponente aus dem DOM entfernt wird. Wenn zum Beispiel eine Schaltfläche aus einem Formular entfernt wird, tritt ein Löscheffekt auf, der die Schaltfläche aus dem DOM entfernt.
- Layout-Effekte
-
Diese Effekte treten auf, bevor der Browser die Möglichkeit hat, zu malen, und werden verwendet, um das Layout der Seite zu aktualisieren. Layout-Effekte werden mit dem
useLayoutEffect
Hook in Funktionskomponenten und dercomponentDidUpdate
Lifecycle-Methode in Klassenkomponenten verwaltet.
Im Gegensatz zu diesen Commit-Phase-Effekten sind passive Effekte benutzerdefinierte Effekte, die so geplant werden, dass sie ausgeführt werden, nachdem der Browser die Möglichkeit hatte, zu malen. Passive Effekte werden mit dem useEffect
Hook verwaltet.
Passive Effekte sind nützlich, um Aktionen auszuführen, die für das anfängliche Rendering der Seite nicht entscheidend sind, wie z. B. das Abrufen von Daten aus einer API oder die Durchführung von Analytics-Tracking. Da passive Effekte nicht während der Rendering-Phase ausgeführt werden, wirken sie sich nicht auf die Zeit aus, die benötigt wird, um einen minimalen Satz von Aktualisierungen zu berechnen, die erforderlich sind, um eine Benutzeroberfläche in den vom Entwickler gewünschten Zustand zu bringen.
Alles auf den Bildschirm bringen
React verwaltet eine FiberRootNode
über beiden Bäumen, die auf einen der beiden Bäume zeigt: den current
oder den workInProgress
Baum. DieFiberRootNode
ist eine wichtige Datenstruktur, die für die Verwaltung der Commit-Phase des Abstimmungsprozesses verantwortlich ist.
Wenn das virtuelle DOM aktualisiert wird, aktualisiert React denworkInProgress
Baum, während der aktuelle Baum unverändert bleibt. So kann React das virtuelle DOM weiter rendern und aktualisieren und gleichzeitig den aktuellen Zustand derAnwendung beibehalten.
Wenn der Rendering-Prozess abgeschlossen ist, ruft React eine Funktion mit dem NamencommitRoot
auf, die dafür verantwortlich ist, die Änderungen am workInProgress
Baum in das aktuelle DOM zu übertragen. commitRoot
wechselt den Zeiger des FiberRootNode
Baums vom aktuellen Baum zumworkInProgress
Baum und macht den workInProgress
Baum zum neuen aktuellen Baum.
Von diesem Zeitpunkt an basieren alle zukünftigen Aktualisierungen auf dem neuen aktuellen Baum. Dieser Prozess stellt sicher, dass die Anwendung in einem konsistenten Zustand bleibt und dass Aktualisierungen korrekt und effizient durchgeführt werden .
All das scheint sofort im Browser zu passieren. Das ist das Werk derVersöhnung.
Kapitel Rückblick
In diesem Kapitel haben wir das Konzept der React Reconciliation erforscht und den Fiber Reconciler kennengelernt. Außerdem haben wir Fibers kennengelernt, die in Verbindung mit einem leistungsstarken Zeitplannungsprogramm ein effizientes und unterbrechbares Rendering ermöglichen. Außerdem lernten wir die Rendering-Phase und die Commit-Phase kennen, die beiden Hauptphasen des Reconciliation-Prozesses. Schließlich lernten wir FiberRootNode
kennen: eine wichtige Datenstruktur, die für die Verwaltung der Commit-Phase des Versöhnungsprozesses verantwortlich ist.
Fragen überprüfen
Stellen wir uns ein paar Fragen, um unser Verständnis der Konzepte in diesem Kapitel zu testen:
-
Was ist React Reconciliation?
-
Welche Rolle spielt die Datenstruktur von Fiber?
-
Warum brauchen wir zwei Bäume?
-
Was passiert, wenn eine Anwendung aktualisiert wird?
Wenn wir diese Fragen beantworten können, sollten wir auf dem besten Weg sein, den Fiber reconciler und den Abstimmungsprozess in React zu verstehen.
Der Nächste
In Kapitel 5 befassen wir uns mit häufigen Fragen in React und erkunden einige fortgeschrittene Muster. Wir beantworten die Fragen, wie oft useMemo
und wann React.lazy
verwendet werden sollte. Außerdem erfahren wir, wieuseReducer
und useContext
zur Verwaltung von Zuständen in React-Anwendungen genutzt werden können.
Wir sehen uns dort!
Get Fluent React now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.