Debajo del capó de React. Escribimos nuestra implementación desde cero

En esta serie de artículos, crearemos nuestra propia implementación de React desde cero. Al final, comprenderá cómo funciona React, qué métodos del ciclo de vida del componente llama y por qué. El artículo está destinado a aquellos que ya han usado React y quieren aprender sobre su dispositivo, o para los muy curiosos.

imagen

Este artículo es una traducción de React Internals, Primera parte: representación básica

Este es en realidad el primer artículo de cada cinco


  1. Conceptos básicos de representación <- estamos aquí
  2. ComponentWillMount y componentDidMount
  3. Actualización
  4. setState
  5. Transacciones

El material se creó cuando React 15.3 era relevante, en particular el uso de ReactDOM y el conciliador de pila. React 16 y superior tiene algunos cambios. Sin embargo, este material sigue siendo relevante, ya que da una idea general de lo que está sucediendo "bajo el capó".

Parte 1. Conceptos básicos de renderizado


Elementos y componentes


Hay tres tipos de entidades en React: un elemento DOM nativo, un elemento React virtual y un componente.

Elementos DOM nativos


Estos son los elementos DOM que utiliza el navegador para crear la página web, por ejemplo, div, span, h1. React los crea llamando a document.createElement () e interactúa con la página utilizando métodos de API de DOM basados ​​en navegador como element.insertBefore (), element.nodeValue y otros.

Elemento de reacción virtual


Un elemento React virtual (a menudo denominado simplemente "elemento") es un objeto javascript que contiene las propiedades necesarias para crear o actualizar un elemento DOM nativo o un árbol de dichos elementos. Según el elemento React virtual, se crean elementos DOM nativos, como div, span, h1 y otros. Podemos decir que un elemento React virtual es una instancia de un componente compuesto definido por el usuario, más sobre esto a continuación.

Componente


Componente es un término bastante general en React. Los componentes son entidades con las que React realiza diversas manipulaciones. Diferentes componentes sirven para diferentes propósitos. Por ejemplo, el ReactDomComponent de la biblioteca ReactDom es responsable de la unión entre los elementos React y sus elementos DOM nativos correspondientes.

Componentes compuestos personalizados


Lo más probable es que ya haya encontrado este tipo de componente. Cuando llama a React.createClass () o usa las clases ES6 a través de Extender React.Component, crea un componente compuesto personalizado. Dicho componente tiene métodos de ciclo de vida, como componentWillMount, shouldComponentUpdate y otros. Podemos redefinirlos para agregar algún tipo de lógica. Además, se crean otros métodos, como mountComponent, acceptComponent. React utiliza estos métodos solo para sus fines internos; no interactuamos con ellos de ninguna manera.

ZanudaMode = on
De hecho, los componentes creados por el usuario inicialmente no están completos. React los envuelve en un ReactCompositeComponentWrapper, que agrega todos los métodos del ciclo de vida a nuestros componentes, después de lo cual React puede administrarlos (insertar, actualizar, etc.).

Reaccionar declarativo


Cuando se trata de componentes personalizados, nuestra tarea es definir las clases de estos componentes, pero no instanciamos estas clases. Reaccionar los crea cuando sea necesario.

Además, no creamos elementos explícitamente usando un estilo imperativo; en cambio, escribimos en un estilo declarativo usando JSX:

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

Este código con marcado JSX es traducido por el compilador a lo siguiente:

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

Es decir, en esencia, se convierte en una construcción imperativa para crear un elemento a través de una llamada explícita a React.createElement (). Pero esta construcción está dentro del método render (), que no llamamos explícitamente, React llamará a este método cuando sea necesario. Por lo tanto, percibir React es igual de declarativo: describimos lo que queremos recibir, y React determina cómo hacerlo.

Escribe tu pequeña reacción


Habiendo recibido la base técnica necesaria, comenzaremos a crear nuestra propia implementación de React. Esta será una versión muy simplificada, llamémosla Feact.

Supongamos que queremos crear una aplicación Feact simple cuyo código se vería así:

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

Primero, divaguemos sobre JSX. Esto es precisamente un "retiro", porque el análisis JSX es un gran tema separado que omitiremos como parte de nuestra implementación de Feact. Si estuviéramos tratando con JSX procesado, veríamos el siguiente código:

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

Es decir, usamos Feact.createElement en lugar de JSX. Entonces implementamos este método:

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

El elemento devuelto es un objeto simple que representa lo que queremos representar.

¿Qué hace Feact.render ()?


Al llamar a Feact.render (), pasamos dos parámetros: qué queremos renderizar y dónde. Este es el punto de partida de cualquier aplicación React. Escribamos una implementación del método render () para Feact:

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

Al completar render (), obtenemos una página web terminada. Los elementos DOM son creados por FeactDOMComponent. Escribamos su implementación:

 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; } } 

El método mountComponent crea un elemento DOM y lo almacena en this._hostNode. No lo usaremos ahora, pero volveremos a esto en las siguientes partes.

La versión actual de la aplicación se puede ver en Fiddle .

Literalmente, 40 líneas de código fueron suficientes para hacer una implementación primitiva de React. Es poco probable que el Feact que creamos conquiste el mundo, pero refleja bien la esencia de lo que está sucediendo bajo el capó de React.

Agregar componentes personalizados


Nuestro Feact debería poder representar no solo elementos en HTML (div, span, etc.), sino también componentes compuestos definidos por el usuario:
El método Feact.createElement () descrito anteriormente está bien actualmente, por lo que no lo repetiré en la lista de códigos.
 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') ); 

Permítame recordarle que si JSX estuviera disponible, llamar al método render () se vería así:

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

Pasamos la clase de componente personalizado a createElement. Un elemento React virtual puede representar un elemento DOM normal o un componente personalizado. Los distinguiremos de la siguiente manera: si pasamos un tipo de cadena, entonces este es un elemento DOM; si es una función, entonces este elemento representa un componente personalizado.

Mejorando Feact.render ()


Si observa detenidamente el código en este momento, verá que Feact.render () no puede procesar componentes personalizados. Vamos a arreglar esto:

 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); } } 

Hemos creado un contenedor para el artículo pasado. Dentro del contenedor, creamos una instancia de la clase de componente de usuario y llamamos a su método componentInstance.render (). El resultado de este método se puede pasar al componente FeactDOMComponent, donde se crearán los elementos DOM correspondientes.

Ahora podemos crear y renderizar componentes personalizados. Feact creará nodos DOM basados ​​en componentes personalizados y los cambiará en función de las propiedades (accesorios) de nuestros componentes personalizados. Esta es una mejora significativa en nuestro Feact.
Tenga en cuenta que FeactCompositeComponentWrapper crea directamente FeactDOMComponent. Una relación tan cercana es mala. Lo arreglaremos más tarde. Si React tenía la misma conexión cercana, entonces solo se podrían crear aplicaciones web. Agregar una capa adicional de ReactCompositeComponentWrapper le permite separar la lógica React para administrar elementos virtuales y la visualización final de elementos nativos, lo que le permite usar React no solo al crear aplicaciones web, sino también, por ejemplo, React Native para dispositivos móviles.

Mejora de componentes personalizados


Los componentes personalizados creados solo pueden devolver elementos DOM nativos, si intentamos devolver otros componentes personalizados, obtenemos un error. Corrija esta falla. Imagine que nos gustaría ejecutar el siguiente código sin errores:

 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); } } } 

El método render () de un componente personalizado puede devolver un elemento DOM nativo u otro componente personalizado. Si la propiedad asTitle es verdadera, entonces FeactCompositeComponentWrapper devolverá el componente personalizado para FeactDOMComponent donde se producirá el error. Arregle 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); } } 

En verdad, ahora hemos hecho una muleta para satisfacer las necesidades actuales. Una llamada al método de representación devolverá componentes secundarios hasta que devuelva un elemento DOM nativo. Esto es malo porque dichos componentes secundarios no participarán en el ciclo de vida. Por ejemplo, en este caso, no podremos implementar la llamada componentWillMount. Lo arreglaremos más tarde.

Y nuevamente arreglamos Feact.render ()


La primera versión de Feact.render () solo podía procesar elementos DOM nativos. Ahora solo los componentes definidos por el usuario se procesan correctamente sin soporte nativo. Es necesario manejar ambos casos. Puede escribir una fábrica que creará un componente dependiendo del tipo de elemento pasado, pero React eligió una forma diferente: simplemente envuelva cualquier componente entrante en otro componente:

 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 es esencialmente un componente personalizado. También se puede definir llamando a Feact.createClass (). Su método de representación simplemente devuelve el elemento que se le pasó. Ahora cada elemento está envuelto en TopLevelWrapper, y FeactCompositeComponentWrapper siempre recibirá un componente personalizado como entrada.

Conclusión de la primera parte.


Hemos implementado Feact, que puede renderizar componentes. El código generado muestra los conceptos básicos de renderizado. La representación real en React es mucho más complicada y cubre eventos, enfoque, desplazamiento de ventanas, rendimiento, etc.

El jsfiddle final de la primera parte.

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


All Articles