Введение
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.
Практика
Возьмём, к примеру, такое приложение:
Его код:
// 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)
Разберём пошагово какие инструкции мы даём компьютеру в такой программе:
- Сначала мы импортируем код библиотек React и ReactDOM;
-
Создаём переменную
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": {} }
-
Создаём
ButtonFiber
:const ButtonFiber = React.createElement('button', null, 'click me') { "type": "button", "key": null, "ref": null, "props": { "children": "click me" }, "_owner": null, "_store": {} }
-
Создаём
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": {} }
-
Получаем референс на контейнер для нашего приложения
document.getElementById('root')
:<html> <div id="root"></div> </html>
- Вызываем рендер приложения:
ReactDOM.render(AppFiber, rootDiv)
. Сообщаем реакту, что надо сгенерировать HTML так, как описано в объектеAppFiber
, и поместить результат вrootDiv
; - React проходит по массиву
AppFiber.children
и создаёт HTML-элементы; -
Создаётся разметка для родительского элемента и к нему добавляются дочерние (
element.appendChild()
):// Copyright (c) Facebook, Inc. and its affiliates. export function appendChild( parentInstance: Instance, child: Instance | TextInstance ): void { parentInstance.appendChild(child) }
- Когда разметка создана, React переносит изменения на страницу, внутрь
rootDiv
; - Браузер отображает изменения.
Вот ссылка на интересное видео, где один из разработчиков 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()
принимает следующие аргументы:
- Название HTML-тэга; функцию, которая возвращает fiber; компонент основанный на классах. Всё, что в JSX мы могли бы записать как
<SomeElement />
; - Объект, который обычно называют
props
. Это то, что наша функция-компонент получит в качестве аргумента; -
Все последующие аргументы — это дочерние компоненты, в любом количестве:
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!'
)
// ...
}
// ...
Проверяем результат и видим, что нажатие на кнопку не оказывает никакого эффекта:
Мы написали баг, а значит пришло время достать главный дебаггер JavaScript разработчика - console.log
. Для начала проверим, вызывается ли функция setToBlue
:
function setToBlue() {
console.log('color before', color)
color = 'dodgerblue'
console.log('color after', color)
}
Как видно на анимации, функция вызывается, переменная меняет своё значение и сохраняет его между нажатиями. Почему же цвет надписи не меняется?
Ответ простой. React — это JavaScript. Наш компонент — это функция. При первом вызове ReactDOM.render
React вызывает наши компоненты-функции, чтобы составить дерево Virtual DOM.
Когда он вызывает функцию getAppFiber
, где-то в памяти компьютера определяется место для переменной color
со значением "crimson"
:
Такое значение используется при создании fiber для заголовка.
Mы жмём на кнопку "click me". Вызывается функция setToBlue
. Какие инструкции для компьютера содержит эта функция? Что мы просим сделать компьютер, когда вызываем её?
color = '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)
}
// ...
}
// ...
Приложение всё ещё не работает, но вывод в консоль изменился. Значит мы на верном пути.
Сейчас значение переменной сбрасывается, а раньше сохранялось. Почему?
Раньше мы просто меняли значение в памяти, а теперь просим 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)
}
// ...
}
Работает!
Кто-то мог заметить, что мы создали собственную версию хука 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.
Пишите программы и будьте здоровы!