React — это JavaScript

2020.09.13

Введение

React может показаться магической штукой. JSX, правила хуков, lifecycle методы могут создать впечатление, что React — не JavaScript, а нечто сверхъестественное. Цель этого поста — повысить мой собственный уровень знаний и через это развеять ореол магии. Это позволит писать код более осознанно.

React — это JavaScript

React — это JavaScript. Это важно помнить. Не какой-то модный JavaScript с proxy и декораторами, а обычный такой JavaScript. Я думаю, именно JSX больше всего мешает осознанию, что React — это JavaScript. Поэтому первым делом развеем магию JSX.

Вот пример компонента:

function MyComponent(props) { return <div>{props.title}</div> }

Когда мы запускаем команду npm run build, сборщик проекта обрабатывает наши файлы, импортирует зависимости, картинки и CSS. Когда он встречает JSX, наш компонент становится таким:

function MyComponent(props) { return React.createElement('div', null, props.title) }

Дополнительно сборщик применяет разные оптимизации, но суть остаётся — JSX разметка заменяется вызовом React.createElement.
Получается, что наш компонент представляет собой обычную JavaScript функцию, которая вызывает другую JavaScript функцию — React.createElement и возвращает результат. Самый обычный JavaScript.

Что возвращает React.createElement?

Начиная с 16-й версии, React.createElement возвращает объект, который React-разработчики называют "fiber". Этот объект содержит необходимую информацию о компоненте, включая свойство type. Данное свойство указывает на тип элемента: HostComponent (для веба это один из HTML тэгов), FunctionComponent, ClassComponent, React.Fragment и проч. Из этих объектов составляется тот самый Virtual DOM.

Практика

Возьмём, к примеру, такое приложение:

Заголовок: "Hello World!", а под ним кнопка "click me!"

Его код:

// 1. Импорты import React from 'react' import ReactDOM from 'react-dom' // 2. Создаём fiber для заголовка const TitleFiber = React.createElement('h1', null, 'Hello World!') // 3. Создаём fiber для кнопки const ButtonFiber = React.createElement('button', null, 'click me') // 4. Создаём fiber для приложения const AppFiber = React.createElement('div', null, TitleFiber, ButtonFiber) // 5. Получаем референс на контейнер для нашего приложения const rootDiv = document.getElementById('root') // 6 - 10. Рендер ReactDOM.render(AppFiber, rootDiv)

Разберём пошагово какие инструкции мы даём компьютеру в такой программе:

  1. Сначала мы импортируем код библиотек React и ReactDOM;
  2. Создаём переменную TitleFiber, которой присваиваем результат выполнения React.createElement('h1', null, 'Hello World!'):

    const TitleFiber = React.createElement('h1', null, 'Hello World!') { "type": "h1", "key": null, "ref": null, "props": { "children": "Hello World!" }, "_owner": null, "_store": {} }
  3. Создаём ButtonFiber:

    const ButtonFiber = React.createElement('button', null, 'click me') { "type": "button", "key": null, "ref": null, "props": { "children": "click me" }, "_owner": null, "_store": {} }
  4. Создаём AppFiber с двумя дочерними элементами: TitleFiber и ButtonFiber. Получается такой объект:

    const AppFiber = React.createElement('div', null, TitleFiber, ButtonFiber) { "type": "div", "key": null, "ref": null, "props": { "children": [ { "type": "h1", "key": null, "ref": null, "props": { "children": "Hello World!" }, "_owner": null, "_store": {} }, { "type": "button", "key": null, "ref": null, "props": { "children": "click me" }, "_owner": null, "_store": {} } ] }, "_owner": null, "_store": {} }
  5. Получаем референс на контейнер для нашего приложения document.getElementById('root'):

    <html> <div id="root"></div> </html>
  6. Вызываем рендер приложения: ReactDOM.render(AppFiber, rootDiv). Сообщаем реакту, что надо сгенерировать HTML так, как описано в объекте AppFiber, и поместить результат в rootDiv;
  7. React проходит по массиву AppFiber.children и создаёт HTML-элементы;
  8. Создаётся разметка для родительского элемента и к нему добавляются дочерние (element.appendChild()):

    // Copyright (c) Facebook, Inc. and its affiliates. export function appendChild( parentInstance: Instance, child: Instance | TextInstance ): void { parentInstance.appendChild(child) }
  9. Когда разметка создана, React переносит изменения на страницу, внутрь rootDiv;
  10. Браузер отображает изменения.

Вот ссылка на интересное видео, где один из разработчиков React подробно рассказывает о том, как работает рендер: SMOOSHCAST: React Fiber Deep Dive with Dan Abramov

Просто. Java. Script.

Сейчас наше приложение статично — в нём нет интерактивности. Давайте проведём небольшой рефактор, а заодно сделаем его ещё больше похожим на обычный JavaScript. Разнесём создание наших fiber по функциям, чтобы передавать в них аргументы, и избавимся от PascaleCase:

import React from 'react' import ReactDOM from 'react-dom' // Заголовок, который отображает текст function getTitleFiber(props) { const fiber = React.createElement('h1', props) return fiber } // Кнопка function getButtonFiber(props) { const fiber = React.createElement('button', props) return fiber } // Корневой компонент function getAppFiber() { const buttonProps = { type: 'button', } const buttonFiber = React.createElement( // Типом элемента может быть и функция (React Function Component) getButtonFiber, // Props должны быть либо объектом, либо null buttonProps, 'click me' ) const titleFiber = React.createElement(getTitleFiber, null, 'Hello World!') const fiber = React.createElement( 'div', null, // Количество children не ограничено // и они могут быть не только строкой, но и другой fiber titleFiber, buttonFiber ) return fiber } const appFiber = React.createElement(getAppFiber) const rootDiv = document.getElementById('root') ReactDOM.render(appFiber, rootDiv)

Как видно из примера, React.createElement() принимает следующие аргументы:

  1. Название HTML-тэга; функцию, которая возвращает fiber; компонент основанный на классах. Всё, что в JSX мы могли бы записать как <SomeElement />;
  2. Объект, который обычно называют props. Это то, что наша функция-компонент получит в качестве аргумента;
  3. Все последующие аргументы — это дочерние компоненты, в любом количестве:

    React.createElement( // Element type 'div', // props { className: 'container' }, // ...children 'This is direct child', React.createElement('h1', null, 'This is nested h1 tag'), React.createElement('p', null, 'This is nested p tag') )

State

Мы провели рефактор. Теперь добавим динамику — сделаем надпись красной, а по нажатию на кнопку она покрасится в синий:

import React from 'react' import ReactDOM from 'react-dom' // ... // Корневой компонент function getAppFiber() { let color = 'crimson' function setToBlue() { color = 'dodgerblue' } const titleProps = { style: { color } } const buttonProps = { type: 'button', onClick: setToBlue, } const buttonFiber = React.createElement( // Типом элемента может быть и функция (React Function Component) getButtonFiber, // Props должны быть либо объектом, либо null buttonProps, 'click me' ) const titleFiber = React.createElement( getTitleFiber, titleProps, 'Hello World!' ) // ... } // ...

Проверяем результат и видим, что нажатие на кнопку не оказывает никакого эффекта: Красная надпись "Hello World" и рядом кнопка "click me". На анимации показано как нажатия на кнопку не оказывают никакого эффекта

Мы написали баг, а значит пришло время достать главный дебаггер JavaScript разработчика - console.log. Для начала проверим, вызывается ли функция setToBlue:

function setToBlue() { console.log('color before', color) color = 'dodgerblue' console.log('color after', color) }

Красная надпись "Hello World" и рядом кнопка "click me". На анимации показано как нажатия на кнопку не оказывают никакого эффекта, а в консоль выводится сначала "crimson", а потом всегда "dodgerblue"

Как видно на анимации, функция вызывается, переменная меняет своё значение и сохраняет его между нажатиями. Почему же цвет надписи не меняется?

Ответ простой. React — это JavaScript. Наш компонент — это функция. При первом вызове ReactDOM.render React вызывает наши компоненты-функции, чтобы составить дерево Virtual DOM.
Когда он вызывает функцию getAppFiber, где-то в памяти компьютера определяется место для переменной color со значением "crimson":

color указывает на crimson
Такое значение используется при создании fiber для заголовка.
Mы жмём на кнопку "click me". Вызывается функция setToBlue. Какие инструкции для компьютера содержит эта функция? Что мы просим сделать компьютер, когда вызываем её?

color = 'dodgerblue'

или

color больше не указывает на crimson, а указывает на dodgerblue
И всё.
Мы просто попросили компьютер изменить значение у одной переменной. Мы не дали команду на обновление Virtual DOM и внесение изменений на экран. Так как React — это JavaScript, он не может магическим образом узнать, что у приложения изменилось состояние и надо запустить рендер. Реакту нужно сообщить об изменении явно. Как это сделать?
Если мы посмотрим ещё раз на код приложения, мы увидим в нижней части файла вызов функции ReactDOM.render:

ReactDOM.render(appFiber, rootDiv)

Похоже это то, что нам нужно!

// Корневой компонент function getAppFiber() { // ... function setToBlue() { console.log('color before', color) color = 'dodgerblue' console.log('color after', color) const appFiber = React.createElement(getAppFiber) const rootDomElement = document.getElementById('root') ReactDOM.render(appFiber, rootDomElement) } // ... } // ...

Красная надпись "Hello World" и рядом кнопка "click me". На анимации показано, что нажатия на кнопку не оказывают никакого эффекта, а в консоль выводится сначала "crimson", потом "dodgerblue", после этого снова "crimson", потом "dodgerblue" и так всё время.

Приложение всё ещё не работает, но вывод в консоль изменился. Значит мы на верном пути.
Сейчас значение переменной сбрасывается, а раньше сохранялось. Почему?
Раньше мы просто меняли значение в памяти, а теперь просим React отрендерить приложение после перезаписи переменной. React рендерит приложение, вызывает функцию getAppFiber(), чтобы получить appFiber:

// ... const appFiber = React.createElement(getAppFiber) // ... ReactDOM.render(appFiber, rootDomElement)

Что делает функция getAppFiber()? Создаёт локальную переменную color и присваивает ей значение "crimson". Каждый раз, вызывая функцию, JavaScript создаёт все локальные переменные заново. А React — это JavaScript. Получается, чтобы решить проблему, надо сделать переменную color глобальной. Вынесем объявление color на уровень выше:

let color = 'crimson' // Корневой компонент function getAppFiber() { function setToBlue() { console.log('color before', color) color = 'dodgerblue' console.log('color after', color) const appFiber = React.createElement(getAppFiber) const rootDomElement = document.getElementById('root') ReactDOM.render(appFiber, rootDomElement) } // ... }

Красная надпись "Hello World" и рядом кнопка "click me". На анимации показано, что после нажатия на кнопку надпись меняет свой цвет на синий, а в консоль выводится сначала "crimson", потом "dodgerblue".

Работает!

Кто-то мог заметить, что мы создали собственную версию хука useState. И если вы думаете, что useState работает похожим образом, то будете совершенно правы. Функция, которую возвращает useState, при вызове запускает процесс рендеринга. В отличие от нашего варианта, в useState применён ряд оптимизаций. Например, если новое состояние не отличается от предыдущего, то React просто переиспользует имеющееся дерево fiber.

Вот полный код нашего приложения:

import React from 'react' import ReactDOM from 'react-dom' // Заголовок, который отображает текст function getTitleFiber(props) { const fiber = React.createElement('h1', props) return fiber } // Кнопка function getButtonFiber(props) { const fiber = React.createElement('button', props) return fiber } let color = 'crimson' // Корневой компонент function getAppFiber() { function setToBlue() { console.log('color before', color) color = 'dodgerblue' console.log('color after', color) const appFiber = React.createElement(getAppFiber) const rootDomElement = document.getElementById('root') ReactDOM.render(appFiber, rootDomElement) } const titleProps = { style: { color } } const buttonProps = { type: 'button', onClick: setToBlue, } const buttonFiber = React.createElement( // Типом элемента может быть и функция (React Function Component) getButtonFiber, // Props должны быть либо объектом, либо null buttonProps, 'click me' ) const titleFiber = React.createElement( getTitleFiber, titleProps, 'Hello World!' ) const fiber = React.createElement( 'div', null, // Количество children не ограничено // и они могут быть не только строкой, но и другой fiber titleFiber, buttonFiber ) return fiber } const appFiber = React.createElement(getAppFiber) const rootDiv = document.getElementById('root') ReactDOM.render(appFiber, rootDiv)

А вот JSX-версия:

import React from 'react' import ReactDOM from 'react-dom' // Заголовок, который отображает текст const Title = ({ children, ...props }) => { return <h1 {...props}>{children}</h1> } // Кнопка const Button = ({ children, ...props }) => { return <button {...props}>{children}</button> } let color = 'crimson' // Корневой компонент const App = () => { function setToBlue() { console.log('color before', color) color = 'dodgerblue' console.log('color after', color) const rootDomElement = document.getElementById('root') ReactDOM.render(<App />, rootDomElement) } return ( <div> <Title style={{ color }}>Hello World!</Title> <Button type="button" onClick={setToBlue}> click me </Button> </div> ) } const rootDiv = document.getElementById('root') ReactDOM.render(<App />, rootDiv)

React Hooks

Хуки могут показаться ещё одним магическим заклинанием в React — у них свои правила и массивы зависимостей. На самом деле хуки — это обычные JavaScript-функции. И ведут они себя соответствующе. Например, вызываются на каждом рендере компонента. Вы можете называть свои хуки как угодно, просто в этом случае подсказки eslint могут не сработать. Подробный разбор того, как работают хуки и почему у них такие особенности — тема для отдельного поста. Если вам не терпится, посмотрите прекрасное видео от Райана Флоренса и Майкла Джексона (авторы React Router): Fun with React Hooks - Michael Jackson and Ryan Florence

Производительность

React применяет множество оптимизаций, благодаря чему работает очень быстро. Если вы хотите больше узнать о способах оптимизации, я рекомендую начать со статьи Райана Флоренса: React, Inline Functions, and Performance.

Заключение

React — это JavaScript. Функциональные компоненты в React — обычные JavaScript-функции, которые живут по тем же законам, что и обычные JavaScript-функции. То же касается React Hooks.

Написание этого поста послужило толчком к более глубокому изучению React. Теперь я лучше понимаю как работает мой код и чувсвтую уверенность в том, что пишу. А отдельное удовольствие мне доставляет то, что я могу оценить красоту библиотеки. Надеюсь этот текст помог и вам развеять магию, которая окружает React.

Пишите программы и будьте здоровы!

При копировании материалов нужно обязательно указать авторство (имя и ссылка на сайт).
контакты