Это шестая часть моих заметок о Руководстве по ReactJS для начинающих от egghead.io.
Используйте компоненты класса с React
В этом разделе будет показано, как неявные привязки this
теряются при обновлении состояния в React и как с этим бороться разными способами. Название видео было бы лучше назвать «Состояние настройки и this
».
Неявное связывание и его потеря
Привязка this
определяется этими правилами в порядке приоритета:
- Если вызывается
new
,this
привязывается к вновь созданному объекту. - Если вызывается
call
илиapply
(илиbind
),this
привязывается к указанному объекту. - Если вызывается объект контекста, владеющий вызовом,
this
привязывается к объекту контекста. this
по умолчанию — этоundefined
в строгом режиме или глобальный объект в противном случае.
Мы сосредоточимся на пункте 3, который называется неявное связывание.
Это ванильный пример JavaScript с неявной привязкой и потерей неявной привязки.
var name = "GLOBAL OBJECT NAME"; function greet (name) { return `Hi ${name}, my name is ${this.name}!` } var bob = { name: 'Bob', greet: greet } bob.greet('Jane'); // Hi Jane, my name is Bob! var greetFn = bob.greet; // this binding is lost! greetFn('Jane'); // // Hi Bob, my name is GLOBAL OBJECT NAME!
Хотя bob
не "владеет" функцией greet
, можно сказать, что она принадлежит в момент вызова bob.greet
. В этой ситуации мы говорим, что bob
является объектом контекста, а this
"неявно" связано с bob
.
Привязка this
к bob
теряется, когда bob.greet
присваивается простой функции greetFn
, потому что greetFn
является простой функцией, которая ссылается на greet
. Применяется привязка по умолчанию, и GLOBAL OBJECT NAME
регистрируется в консоли.
Важно отметить, что хотя greetFn
выглядит как ссылка на bob.greet
, на самом деле это просто ссылка на функцию greet
.
Давайте посмотрим на что-то подобное:
var bob = { name: 'Bob', greet(name) { return `Hi ${name}, my name is ${this.name}`; } } bob.greet('Jane'); // "Hi Jane, my name is Bob" var greetFn = bob.greet; greetFn('Jane'); // "Hi Jane, my name is !"
Поскольку глобальной переменной name
нет, ничего не возвращается.
Теперь мы можем понять, как происходит нечто подобное при обновлении состояния в React, и как с этим бороться.
Потеря привязки this
в компонентах класса
Мы начнем с создания кнопки, которая увеличивает число при нажатии.
class Counter extends React.Component { constructor(...args) { super(...args) this.state = {count: 0} } render() { return ( <button onClick={() => this.setState(({count}) => ({ count: count + 1, }))} > {this.state.count} </button> ) } } ReactDOM.render( <Counter />, document.getElementById('root'), )
Сосредоточьтесь на значении onClick
. У нас есть стрелочная функция, которая увеличивает состояние на this.setState
.
Хотя этот код полностью функционален, мы могли бы назначить функцию this.setState
обработчику событий для большей удобочитаемости.
Отредактированный код будет выглядеть так:
handleClick() { this.setState(({count}) => ({ count: count + 1, })) } render() { return ( <button onClick={this.handleClick} > {this.state.count} </button> ) }
О, о! Это больше не работает. Если мы откроем консоль разработчика, то получим Uncaught TypeError: Cannot read property 'setState' of undefined
.
Привязка this
в this.setState
к объекту, созданному классом Counter
, была потеряна, когда мы присвоили его handleClick()
. Мы просто присваиваем ссылку на функцию setState
handleClick()
. handleClick()
тоже обычная функция. Поскольку классы в ES6 всегда выполняются в строгом режиме, this
по умолчанию равно undefined
. Обратите внимание на параллели с приведенным выше примером в JavaScript.
Сохранение привязки this
в компонентах класса
Мы рассмотрим несколько способов гарантировать, что привязка this
не потеряется.
Один из способов использует «явную привязку» или правило номер 2 из приведенных выше this
правил привязки. Это когда мы явно говорим, к чему мы хотим привязать this
, с помощью call
, apply
или bind
.
Здесь мы будем использовать встроенную утилиту bind
в задании onClick
. bind
возвращает новую функцию, которая жестко закодирована для вызова исходной функции с указанным вами объектом контекста this
. Этот вариант явного связывания, включающий назначение выражений call
, apply
или bind
функциям, называется «жестким связыванием».
Таймер будет работать правильно, если мы отредактируем onClick
следующим образом:
onClick={this.handleClick.bind(this)}
Хотя это работает, в некоторых ситуациях это может быть узким местом в производительности.
Чтобы устранить узкое место, мы можем сослаться на прототипный метод handleClick()
, добавив this.handleClick
в конструктор и назначив ему предварительно привязанный метод handleClick
.
Конструктор будет выглядеть так:
constructor(...args) { super(...args) this.state = {count: 0} this.handleClick = this.handleClick.bind(this) }
Это то же самое, что и общий шаблон лексического захвата this
в коде до ES6, например, в var self = this;
.
Это решение может быть громоздким, если нам нужно сделать это для множества методов.
Давайте посмотрим на другое решение с открытыми полями класса. В двух словах, это позволяет нам объявлять свойства класса без конструктора.
Посмотрите на приведенный ниже код до и после реализации полей общедоступного класса.
До:
constructor(...args) { super(...args) this.state = {count: 0} this.handleClick = this.handleClick.bind(this) } handleClick() { this.setState(({count}) => ({ count: count + 1, })) }
После:
state = {count: 0} handleClick = this.handleClick.bind(this) handleClick() { this.setState(({count}) => ({ count: count + 1, })) }
Мы переместили присваивания this.state
и this.handeClick
из конструктора в тело класса. this.
больше не нужен, так как state
и handleClick
находятся в теле класса. state
и handeClick
— это общедоступные поля класса, и мы присваиваем им выражения. Поскольку конструктор стал таким же, как конструктор по умолчанию, мы удалили его.
Увеличивающий счетчик будет работать с этой реализацией.
Мы могли бы реорганизовать это, удалив метод handleClick()
из прототипа и сохранив его только в экземпляре.
state = {count: 0} handleClick = function() { this.setState(({count}) => ({ count: count + 1, })) }.bind(this)
Пока это работает, мы должны вызывать метод .bind
каждый раз, когда мы обновляем состояние. Мы можем использовать лексический this
, который возможен со стрелочными функциями, чтобы обойти это.
handleClick = () => { this.setState(({count}) => ({ count: count + 1, })) }
Какой способ лучше?
Мы обсудили четыре основных способа:
- Жесткая привязка метода с
.bind
в значении назначения обработчика событий - Лексический захват метода прототипа в конструкторе
- Вариант № 2 с использованием полей общедоступного класса
- Использование лексического
this
через стрелочные функции
Проблема с номером 1 заключается в том, что в некоторых ситуациях он может быть узким местом в производительности. Номер 2 может быть громоздким, если нам нужно сделать это для многих методов. Номер 3 в настоящее время является экспериментальной реализацией; это, вероятно, не очень хорошо для производственного кода (хотя это обычно используется во многих проектах).
Номер 4 работает и очень распространен, но не без критики. Автор YDKJS Кайл Симпсон говорит, что использование лексического this
укрепляет плохую практику уклонения от четкого понимания this
. Он предлагает людям либо придерживаться кода лексического стиля, либо принять this
и избегать лексического this
.
В любом случае важно понимать как жесткую привязку, так и лексику this
, чтобы понимать код других и адаптироваться к любой ситуации при работе в команде.
TL;DR
Будьте осторожны, чтобы не потерять привязку this
при обновлении состояния в компонентах класса. Вы можете сохранить привязку либо с помощью метода .bind
, захватив лексическое this
с помощью присваивания в конструкторе или с помощью полей общедоступного класса, либо с помощью лексического this
, включенного в стрелочные функции.
Управляйте DOM с помощью React refs
В этом разделе объясняется, как манипулировать узлом DOM напрямую с помощью свойства React ref
. Иногда это необходимо для работы некоторых библиотек JavaScript или когда мы хотим получить значение полей формы.
Мы будем использовать vanilla-tilt.js в качестве примера того, как сделать библиотеку JavaScript функциональной с помощью ref
.
Создание статического изображения
Это наш базовый код:
class Tilt extends React.Component { render() { return ( <div className="tilt-root"> <div className="tilt-child"> <div {...this.props} /> </div> </div> ) } } const element = ( <div className="totally-centered"> <Tilt> <div className="totally-centered"> vanilla-tilt.js </div> </Tilt> </div> ) ReactDOM.render( element, document.getElementById('root'), )
Класс Tilt
отображает div с классом tilt-root
, в который вложен div с классом tilt-child
, в который вложен div, распространяющий свойства. tilt-root
стилизует один div с цветным градиентом, а tilt-child
представляет собой меньшую белую рамку. У обоих также есть стили для анимации, которые мы увидим позже.
Затем мы создаем константу element
, которая представляет собой компонент Tilt
, заключенный в div с классом totally-centered
. Внутри Tilt
есть еще один div с тем же классом со словами «vanilla-tilt.js». totally-centered
— это стили flexbox, которые центрируют контент по горизонтали и вертикали.
Этот код создает это статическое изображение:
Добавление ref
Взято из React docs,
Атрибут ref принимает функцию обратного вызова, и обратный вызов будет выполнен сразу после монтирования или размонтирования компонента.
Когда атрибут ref используется в элементе HTML, обратный вызов ref получает базовый элемент DOM в качестве аргумента.
В нашем коде мы будем использовать обратный вызов ref
для хранения ссылки на желаемый узел DOM.
Метод рендеринга теперь возвращает это:
return ( <div ref={node => (this.rootNode = node)} className="tilt-root"> <div className="tilt-child"> <div {...this.props} /> </div> </div>
Хорошей практикой является не назначать стрелочные функции ссылкам, так как назначения стрелочных функций создаются заново при каждом повторном рендеринге и по другим причинам.
Мы могли бы переписать код так:
setRef = node => { return this.rootNode = node } render() { return ( <div ref={this.setRef}
Мы видим, что обращаемся к нужному узлу:
componentDidMount() { console.log(this.rootNode) }
Использование узла с библиотекой
Теперь мы можем использовать this.rootNode
с библиотекой vanilla-tilt.
Вносим библиотеку vanilla-tilt со скриптом <script src="https://unpkg.com/[email protected]/dist/vanilla-tilt.min.js"></script>
.
Затем мы можем использовать библиотеку vanilla-tilt, поместив VanillaTilt
global в метод ComponentDidMount
, потому что мы можем получить доступ к узлу после монтирования компонента. Мы инициализируем VanillaTilt
с помощью init
, затем передаем некоторые параметры узлу (this.rootNode
). Наш новый код выглядит так:
componentDidMount() { VanillaTilt.init(this.rootNode,{ max: 25, speed: 400, glare: true, 'max-glare': 0.5, }) }
Оно работает!
TL;DR
Чтобы управлять DOM,
- Передайте
ref
элементу, который вы визуализируете. Помещениеref
в класс ссылается на экземпляр этого класса. - В значении
ref
передайте узел в качестве аргумента и верните назначение узла значению в экземпляре (например:this.rootNode
). - После того, как компонент смонтирован, узел можно использовать для библиотеки или чего-то еще.
Редактировать: Спасибо Эдди Уилсону за совет по объявлению ссылок в теле класса!