Подробное описание Pure Component и React.memo () и зачем они нам нужны
React отображает компоненты гораздо больше, чем вы думаете. Давай узнаем почему.
Распространенная ошибка - полагать, что компонент React достаточно умен, чтобы понять, нужно ли ему снова рендерить себя или нет, основываясь на своих собственных props
и state
. Но это не совсем так.
Чтобы лучше понять, почему, мы должны начать с изучения ограничений компонента, не являющегося чистым React, с двух примеров. После этого будет легче понять преимущества PureComponent
и React.memo()
, посмотрев на два других примера.
Краткое описание того, что нас ждет:
- Пример 1. Два компонента класса
- Примирение
- Пример 2 - Два функциональных компонента с хуками
- Пример 3 - один компонент чистого класса + один компонент класса
- Пример 4 - Один чисто функциональный компонент + один функциональный компонент
- Еще кое-что
- Выводы
Пример 1. Два компонента класса
import React, { Fragment } from "react"; | |
class WelcomeMessage extends React.Component { | |
render() { | |
console.log("render WelcomeMessage"); | |
return <label>Hello {this.props.user}</label>; | |
} | |
} | |
class Page extends React.Component { | |
state = { user: "" }; | |
render() { | |
console.log("render Page"); | |
return ( | |
<Fragment> | |
<WelcomeMessage user={this.state.user} /> | |
<button onClick={() => this.setState({ user: "Jack" })}> | |
Set User | |
</button> | |
</Fragment> | |
); | |
} | |
} |
Этот код довольно прост: когда Page
загружен, значение user
в объекте state
является пустой строкой. Только после первого щелчка по кнопке устанавливается значение «Джек».
Итак, что вы ожидаете, что будет напечатано в консоли после первого нажатия кнопки?
Вероятно, вы правильно догадались: поскольку объект state
обновляется, функция render
будет вызвана снова, таким образом, оба console.log
сообщения будут распечатаны.
Но как вы думаете, что произойдет, если вы нажмете кнопку второй, третий или более раз?
Ответ на этот вопрос менее тривиален.
Вы можете подумать, что ничего не будет отображаться снова, поскольку похоже, что объект state
не изменился. Следовательно, в консоли ничего не должно выводиться.
В конце концов, документация напоминает нам, что React повторно визуализирует компонент, только если его props
или state
изменились.
Но этого не происходит. В нашем примере каждый компонент будет повторно отображаться каждый раз, когда вы нажимаете кнопку, и оба сообщения будут печататься в консоли снова и снова.
Помните: в React и props
, и state
являются неизменяемыми объектами.
Это означает, во-первых, что объект state
в Page
фактически изменяется. Скорее, он воссоздается setState
казнью.
Во-вторых, из-за повторного рендеринга Page
WelcomeMessage
будет вызываться снова с новым экземпляром своего props
объекта, который представляет собой совершенно новый объект.
Подсказка - если вы не знакомы с этой концепцией, попробуйте вставить
{} === {}
в консоль и проверьте результат. Также вы можете ознакомиться с этой статьей.
Короче говоря, в нашем примере все компоненты будут визуализироваться снова и снова, даже если в этом нет необходимости.
Теперь вы видите ограничения компонента React?
Прежде чем мы перейдем к следующему примеру, важно указать, что именно React делает при каждом рендеринге.
Примирение
В React для рендеринга означает, что вызывается метод render()
, но сам метод не обновляет DOM напрямую. Вместо этого React запускает процесс, называемый согласованием, в котором он может понять, какие части DOM следует обновить. Это делается путем сравнения предыдущей модели DOM со следующей.
В процессе согласования используется метод, называемый Virtual DOM, который невероятно быстр, поэтому обычно нам не нужно беспокоиться об этом, и наши компоненты работают так, как ожидалось. Однако, когда происходит много ненужного рендеринга, согласование может замедлить работу вашего приложения.
Чтобы избежать ненужных визуализаций и, таким образом, согласования, мы можем использовать PureComponent
(с компонентами класса) или React.memo()
(с компонентами функций).
Скоро мы увидим их в действии, но сначала давайте посмотрим на следующее.
Пример 2 - Два функциональных компонента с крючками
В этом примере используется та же логика, что и в примере 1, но используются функциональные компоненты и перехватчики.
import React, { useState, Fragment } from "react"; | |
const WelcomeMessage = ({ user }) => { | |
console.log("render WelcomeMessage"); | |
return <label>Hello {user}</label>; | |
}; | |
const Page = () => { | |
const [user, setUser] = useState(); | |
console.log("render Page"); | |
return ( | |
<Fragment> | |
<WelcomeMessage user={user} /> | |
<button onClick={() => setUser("Jack")}> | |
Set user | |
</button> | |
</Fragment> | |
); | |
}; |
Давайте сразу перейдем к поведению приложения при нажатии кнопки второй или более раз.
Можно ожидать, что результат будет таким же, как и в компоненте класса в предыдущем примере. Но это не так. Почему?
Здесь состояние компонента, равное user
, больше не является объектом, а представляет собой простую строку.
Помните, как React раньше сравнивал два объекта?
В данном случае сравниваются две строки, которые имеют одинаковое значение Джек. Проверка равенства, используемая React, затем вернет true
, и поэтому рендеринга не произойдет. Бум!
И, поскольку Page
не отображается, WelcomeMessage
не вызывается и не отображается снова.
Здесь мы можем обнаружить одно из преимуществ использования хуков 😎
Обратите внимание: при втором щелчке вы фактически увидите журнал рендеринга страницы, но после третьего щелчка такое поведение не повторится. Как указано в официальной документации, React все еще может потребоваться отрендерить компонент (в котором используется ловушка) в последний раз.
А что, если тип user
должен быть объектом, а не строкой?
Что ж, в этом случае поведение, к сожалению, будет точно таким же, как и в компоненте класса.
В следующих двух примерах будут рассмотрены чистые компоненты и то, как они снимают ограничения, которые мы только что видели.
Пример 3 - Один компонент чистого класса + один компонент класса
Следующий код точно такой же, как и в первом примере, за исключением компонента Page
, который расширяет React.PureComponent
вместо React.Component
.
import React, { Fragment } from "react"; | |
class WelcomeMessage extends React.Component { | |
render() { | |
console.log("render WelcomeMessage"); | |
return <label>Hello {this.props.user}</label>; | |
} | |
} | |
class Page extends React.PureComponent { | |
state = {}; | |
render() { | |
console.log("render Page"); | |
return ( | |
<Fragment> | |
<WelcomeMessage user={this.state.user} /> | |
<button onClick={() => this.setState({ user: "Jack" })}> | |
Set User | |
</button> | |
</Fragment> | |
); | |
} | |
} |
Опять же, как вы ожидаете, что приложение будет вести себя при нажатии кнопки второй или более раз?
Поведение, наконец, является тем, что мы ожидали увидеть с самого начала: второй щелчок по кнопке не приводит к появлению журнала и, следовательно, рендеринга не происходит.
Итак, как именно работает PureComponent
?
В PureComponent
каждый раз при изменении свойств или состояния React выполняет поверхностное сравнение между объектами props
и nextProps
, а также между объектами state
и nextState
.
Мелкое сравнение - это сравнение между всеми ключами в первый уровень в объектах.
В нашем сценарии после второго нажатия кнопки чистый компонент Page
выполнит неглубокое сравнение state
, и, поскольку он не изменился, компонент больше не будет отображаться. Следовательно, компонент WelcomeMessage
тоже не будет.
Последний пример аналогичен, но я хотел бы остановиться на нескольких интересных моментах.
Пример 4 - Один чистый функциональный компонент + один функциональный компонент
Следующий код похож на Пример 2, за исключением того, что:
- Компонент функции
WelcomeMessage
теперь обернут вокругReact.memo()
- Значение
user
- это не строка, а объект
import React, { useState, Fragment } from "react"; | |
const WelcomeMessage = React.memo(({ name }) => { | |
console.log("render WelcomeMessage"); | |
return <label>Hello {name}</label>; | |
}); | |
const Page = () => { | |
const [user, setUser] = useState({}); | |
console.log("render Page"); | |
return ( | |
<Fragment> | |
<WelcomeMessage name={user.name} /> | |
<button onClick={() => setUser({ name: "Jack", age: 42 })}> | |
Set user | |
</button> | |
</Fragment> | |
); | |
}; |
Перво-наперво: React.memo()
выполняет то же самое, что и PureComponent
, но не проверяет наличие изменений в state
, а только в props
.
Важно отметить, что чистый компонент в третьем примере был Page
, а теперь это WelcomeMessage
.
Итак, в последний раз, что вы ожидаете увидеть после второго щелчка по кнопке?
Ответ - только сообщение журнала «отобразить страницу».
Думаю, вы уже знаете причину, но, конечно же:
user
теперь объект. Это вызовет повторную визуализациюPage
при каждом вызовеsetUser
.WelcomeMessage
вместо этого используетReact.memo()
. Каждый раз, когда компонент визуализируется родителем, будет выполняться поверхностное сравнение. Всеprops
, то есть только строкаname
, будут сравниваться, чтобы решить, нужно ли визуализировать компонент или нет.
Еще кое-что
В некоторых случаях поверхностного сравнения, выполняемого React, недостаточно для вашей цели. Представьте себе, например, глубоко вложенный объект.
В этих случаях вы можете сделать следующее:
Для PureComponent
существует метод API под названием shouldComponentUpdate
. Вы можете использовать его для написания собственной логики сравнения. Ваша логика должна тогда вернуть true
, если компонент необходимо обновить, false
, если этого не произошло.
shouldComponentUpdate(nextProps, nextState){ | |
// your logic here | |
} |
Для функционального компонента React.memo(Component, areEqual)
принимает в качестве второго аргумента функцию сравнения areEqual
. Он должен возвращать true
, если компонент не нужно обновлять, false
, если это необходимо.
function areEqual(){ | |
// your logic here | |
} | |
React.memo(MyWelcomeMessage, areEqual) |
Выводы
Ответственно подходите к использованию чистых компонентов. Поверхностное сравнение не дается бесплатно. Это дорогостоящее вычисление, и его лучше избегать, если в нем нет необходимости.
Не преобразовывайте каждый компонент в чистый, это может привести к снижению производительности.
Вместо этого начните с попытки преобразовать компонент в чистый, если он отображает много ненужных операций.
Если вы не знакомы с чистыми компонентами, советуем полагаться на них только в случае возникновения проблем с производительностью.
Спасибо за чтение и удачного кодирования!
Спасибо Алессио Ван Кеулену за вычитку статьи.