Unter der Haube von React. Wir schreiben unsere Implementierung von Grund auf neu

In dieser Artikelserie erstellen wir unsere eigene React-Implementierung von Grund auf neu. Am Ende haben Sie ein Verständnis dafür, wie React funktioniert, welche Methoden des Komponentenlebenszyklus aufgerufen werden und warum. Der Artikel richtet sich an diejenigen, die React bereits verwendet haben und mehr über sein Gerät erfahren möchten, oder an Neugierige.

Bild

Dieser Artikel ist eine Übersetzung von React Internals, Teil 1: Grundlegendes Rendering

Dies ist tatsächlich der erste von fünf Artikeln


  1. Grundlagen des Renderns <- wir sind hier
  2. ComponentWillMount und componentDidMount
  3. Update
  4. setState
  5. Transaktionen

Material wurde erstellt, als React 15.3 relevant war, insbesondere die Verwendung von ReactDOM und Stack Reconciler. Reaktion 16 und höher hat einige Änderungen. Dieses Material bleibt jedoch relevant, da es eine allgemeine Vorstellung davon gibt, was „unter der Haube“ geschieht.

Teil 1. Grundlagen des Renderns


Elemente und Komponenten


In React gibt es drei Arten von Entitäten: ein natives DOM-Element, ein virtuelles React-Element und eine Komponente.

Native DOM-Elemente


Dies sind die DOM-Elemente, mit denen der Browser die Webseite erstellt, z. B. div, span, h1. React erstellt sie durch Aufrufen von document.createElement () und interagiert mit der Seite mithilfe browserbasierter DOM-API-Methoden wie element.insertBefore (), element.nodeValue und anderen.

Virtuelles Reaktionselement


Ein virtuelles React-Element (oft einfach als „Element“ bezeichnet) ist ein Javascript-Objekt, das die erforderlichen Eigenschaften enthält, um ein natives DOM-Element oder einen Baum solcher Elemente zu erstellen oder zu aktualisieren. Basierend auf dem virtuellen React-Element werden native DOM-Elemente wie div, span, h1 und andere erstellt. Wir können sagen, dass ein virtuelles React-Element eine Instanz einer benutzerdefinierten zusammengesetzten Komponente ist, mehr dazu weiter unten.

Komponente


Komponente ist ein ziemlich allgemeiner Begriff in React. Komponenten sind Entitäten, mit denen React verschiedene Manipulationen durchführt. Unterschiedliche Komponenten dienen unterschiedlichen Zwecken. Beispielsweise ist die ReactDomComponent aus der ReactDom-Bibliothek für die Bindung zwischen React-Elementen und ihren entsprechenden nativen DOM-Elementen verantwortlich.

Benutzerdefinierte zusammengesetzte Komponenten


Höchstwahrscheinlich sind Sie bereits auf diese Art von Komponente gestoßen. Wenn Sie React.createClass () aufrufen oder ES6-Klassen verwenden, indem Sie React.Component erweitern, erstellen Sie eine benutzerdefinierte zusammengesetzte Komponente. Eine solche Komponente verfügt über Lebenszyklusmethoden wie componentWillMount, shouldComponentUpdate und andere. Wir können sie neu definieren, um eine Art Logik hinzuzufügen. Darüber hinaus werden andere Methoden erstellt, z. B. mountComponent, receiveComponent. Diese Methoden werden von React nur für interne Zwecke verwendet, wir interagieren in keiner Weise mit ihnen.

ZanudaMode = on
Tatsächlich sind vom Benutzer erstellte Komponenten zunächst nicht vollständig. React verpackt sie in einen ReactCompositeComponentWrapper, der unseren Komponenten alle Lebenszyklusmethoden hinzufügt, wonach React sie verwalten kann (Einfügen, Aktualisieren usw.).

Deklarativ reagieren


Wenn es um benutzerdefinierte Komponenten geht, besteht unsere Aufgabe darin, die Klassen dieser Komponenten zu definieren, aber wir instanziieren diese Klassen nicht. React erstellt sie bei Bedarf.

Außerdem erstellen wir keine expliziten Elemente mit einem imperativen Stil, sondern schreiben mit JSX in einem deklarativen Stil:

class MyComponent extends React.Component { render() { return <div>hello</div>; } } 

Dieser Code mit JSX-Markup wird vom Compiler in Folgendes übersetzt:

 class MyComponent extends React.Component { render() { return React.createElement('div', null, 'hello'); } } 

Das heißt, es wird im Wesentlichen zu einem zwingenden Konstrukt zum Erstellen eines Elements durch einen expliziten Aufruf von React.createElement (). Diese Konstruktion befindet sich jedoch in der render () -Methode, die wir nicht explizit aufrufen. React ruft diese Methode bei Bedarf selbst auf. React wahrzunehmen ist daher genauso deklarativ: Wir beschreiben, was wir empfangen möchten, und React bestimmt, wie es geht.

Schreiben Sie Ihre kleine Reaktion


Nachdem wir die erforderliche technische Basis erhalten haben, werden wir beginnen, unsere eigene React-Implementierung zu erstellen. Dies wird eine sehr vereinfachte Version sein, nennen wir es Feact.

Angenommen, wir möchten eine einfache Feact-Anwendung erstellen, deren Code folgendermaßen aussehen würde:

 Feact.render(<h1>hello world</h1>, document.getElementById('root')); 

Lassen Sie uns zunächst über JSX abschweifen. Dies ist genau ein „Rückzug“, da das JSX-Parsing ein separates großes Thema ist, das wir im Rahmen unserer Feact-Implementierung weglassen werden. Wenn wir uns mit verarbeitetem JSX befassen würden, würden wir den folgenden Code sehen:

 Feact.render( Feact.createElement('h1', null, 'hello world'), document.getElementById('root') ); 

Das heißt, wir verwenden Feact.createElement anstelle von JSX. Also implementieren wir diese Methode:

 const Feact = { createElement(type, props, children) { const element = { type, props: props || {} }; if (children) { element.props.children = children; } return element; } }; 

Das zurückgegebene Element ist ein einfaches Objekt, das darstellt, was wir rendern möchten.

Was macht Feact.render ()?


Durch Aufrufen von Feact.render () übergeben wir zwei Parameter: Was wir rendern möchten und wo. Dies ist der Ausgangspunkt jeder React-Anwendung. Schreiben wir eine Implementierung der render () -Methode für Feact:

 const Feact = { createElement() { /*   */ }, render(element, container) { const componentInstance = new FeactDOMComponent(element); return componentInstance.mountComponent(container); } }; 

Nach Abschluss von render () erhalten wir eine fertige Webseite. DOM-Elemente werden von FeactDOMComponent erstellt. Schreiben wir die Implementierung:

 class FeactDOMComponent { constructor(element) { this._currentElement = element; } mountComponent(container) { const domElement = document.createElement(this._currentElement.type); const text = this._currentElement.props.children; const textNode = document.createTextNode(text); domElement.appendChild(textNode); container.appendChild(domElement); this._hostNode = domElement; return domElement; } } 

Die mountComponent-Methode erstellt ein DOM-Element und speichert es in this._hostNode. Wir werden es jetzt nicht verwenden, aber wir werden in den folgenden Teilen darauf zurückkommen.

Die aktuelle Version der Anwendung kann in Geige angezeigt werden.

Buchstäblich 40 Codezeilen reichten aus, um eine primitive Implementierung von React durchzuführen. Es ist unwahrscheinlich, dass die von uns geschaffene Funktion die Welt erobert, aber sie spiegelt gut die Essenz dessen wider, was unter der Haube von React geschieht.

Benutzerdefinierte Komponenten hinzufügen


Unser Feact sollte nicht nur HTML-Elemente (div, span usw.) rendern können, sondern auch benutzerdefinierte zusammengesetzte Komponenten:
Die zuvor beschriebene Feact.createElement () -Methode ist derzeit in Ordnung, daher werde ich sie in der Codeliste nicht wiederholen.
 const Feact = { createClass(spec) { function Constructor(props) { this.props = props; } Constructor.prototype.render = spec.render; return Constructor; }, render(element, container) { //      //   , //    } }; const MyTitle = Feact.createClass({ render() { return Feact.createElement('h1', null, this.props.message); } }; Feact.render({ Feact.createElement(MyTitle, { message: 'hey there Feact' }), document.getElementById('root') ); 

Wenn JSX verfügbar wäre, würde der Aufruf der render () -Methode folgendermaßen aussehen:

 Feact.render( <MyTitle message="hey there Feact" />, document.getElementById('root') ); 

Wir haben die benutzerdefinierte Komponentenklasse an createElement übergeben. Ein virtuelles React-Element kann entweder ein reguläres DOM-Element oder eine benutzerdefinierte Komponente darstellen. Wir werden sie wie folgt unterscheiden: Wenn wir einen Zeichenfolgentyp übergeben, ist dies ein DOM-Element; Wenn es sich um eine Funktion handelt, repräsentiert dieses Element eine benutzerdefinierte Komponente.

Verbesserung von Feact.render ()


Wenn Sie sich den Code im Moment genau ansehen, werden Sie feststellen, dass Feact.render () keine benutzerdefinierten Komponenten verarbeiten kann. Lassen Sie uns das beheben:

 Feact = { render(element, container) { const componentInstance = new FeactCompositeComponentWrapper(element); return componentInstance.mountComponent(container); } } class FeactCompositeComponentWrapper { constructor(element) { this._currentElement = element; } mountComponent(container) { const Component = this._currentElement.type; const componentInstance = new Component(this._currentElement.props); const element = componentInstance.render(); const domComponentInstance = new FeactDOMComponent(element); return domComponentInstance.mountComponent(container); } } 

Wir haben einen Wrapper für das übergebene Element erstellt. Innerhalb des Wrappers erstellen wir eine Instanz der Benutzerkomponentenklasse und rufen deren componentInstance.render () -Methode auf. Das Ergebnis dieser Methode kann an FeactDOMComponent übergeben werden, wo die entsprechenden DOM-Elemente erstellt werden.

Jetzt können wir benutzerdefinierte Komponenten erstellen und rendern. Feact erstellt DOM-Knoten basierend auf benutzerdefinierten Komponenten und ändert sie abhängig von den Eigenschaften (Requisiten) unserer benutzerdefinierten Komponenten. Dies ist eine signifikante Verbesserung unserer Funktion.
Beachten Sie, dass der FeactCompositeComponentWrapper die FeactDOMComponent direkt erstellt. Eine so enge Beziehung ist schlecht. Wir werden das später beheben. Wenn React dieselbe enge Verbindung hätte, könnten nur Webanwendungen erstellt werden. Durch Hinzufügen einer zusätzlichen Ebene Mit ReactCompositeComponentWrapper können Sie die React-Logik für die Verwaltung virtueller Elemente und die endgültige Anzeige nativer Elemente trennen. So können Sie React nicht nur beim Erstellen von Webanwendungen verwenden, sondern beispielsweise auch React Native für Mobilgeräte.

Benutzerdefinierte Komponentenerweiterung


Erstellte benutzerdefinierte Komponenten können nur native DOM-Elemente zurückgeben. Wenn wir versuchen, andere benutzerdefinierte Komponenten zurückzugeben, wird eine Fehlermeldung angezeigt. Korrigieren Sie diesen Fehler. Stellen Sie sich vor, wir möchten den folgenden Code fehlerfrei ausführen:

 const MyMessage = Feact.createClass({ render() { if (this.props.asTitle) { return Feact.createElement(MyTitle, { message: this.props.message }); } else { return Feact.createElement('p', null, this.props.message); } } } 

Die render () -Methode einer benutzerdefinierten Komponente kann entweder ein natives DOM-Element oder eine andere benutzerdefinierte Komponente zurückgeben. Wenn die asTitle-Eigenschaft true ist, gibt der FeactCompositeComponentWrapper die benutzerdefinierte Komponente für die FeactDOMComponent zurück, in der der Fehler auftritt. Fix FeactCompositeComponentWrapper:

 class FeactCompositeComponentWrapper { constructor(element) { this._currentElement = element; } mountComponent(container) { const Component = this._currentElement.type; const componentInstance = new Component(this._currentElement.props); let element = componentInstance.render(); while (typeof element.type === 'function') { element = (new element.type(element.props)).render(); } const domComponentInstance = new FeactDOMComponent(element); domComponentInstance.mountComponent(container); } } 

In Wahrheit haben wir jetzt eine Krücke gebaut, um den aktuellen Bedürfnissen gerecht zu werden. Ein Aufruf der Rendermethode gibt untergeordnete Komponenten zurück, bis ein natives DOM-Element zurückgegeben wird. Dies ist schlecht, da solche untergeordneten Komponenten nicht am Lebenszyklus teilnehmen. In diesem Fall können wir beispielsweise den Aufruf von componentWillMount nicht implementieren. Wir werden das später beheben.

Und wieder reparieren wir Feact.render ()


Die erste Version von Feact.render () konnte nur native DOM-Elemente verarbeiten. Jetzt werden nur benutzerdefinierte Komponenten ohne native Unterstützung korrekt verarbeitet. Es ist notwendig, beide Fälle zu behandeln. Sie können eine Factory schreiben, die abhängig von der Art des übergebenen Elements eine Komponente erstellt. React hat jedoch einen anderen Weg gewählt: Wickeln Sie einfach alle eingehenden Komponenten in eine andere Komponente ein:

 const TopLevelWrapper = function(props) { this.props = props; }; TopLevelWrapper.prototype.render = function() { return this.props; }; const Feact = { render(element, container) { const wrapperElement = this.createElement(TopLevelWrapper, element); const componentInstance = new FeactCompositeComponentWrapper(wrapperElement); //   } }; 

TopLevelWrapper ist im Wesentlichen eine benutzerdefinierte Komponente. Sie kann auch durch Aufrufen von Feact.createClass () definiert werden. Die Render-Methode gibt einfach das übergebene Element zurück. Jetzt wird jedes Element in TopLevelWrapper eingeschlossen, und FeactCompositeComponentWrapper erhält immer eine benutzerdefinierte Komponente als Eingabe.

Abschluss des ersten Teils


Wir haben Feact implementiert, mit dem Komponenten gerendert werden können. Der generierte Code zeigt die Grundkonzepte des Renderns. Reales Rendern in React ist viel komplizierter und umfasst Ereignisse, Fokus, Bildlauf, Leistung usw.

Die letzte Geige des ersten Teils.

Source: https://habr.com/ru/post/de458916/


All Articles