
Працюючи над React, ви, напевно, стикалися з контрольованими компонентами та обробниками подій. Нам потрібно прив’язати ці методи до екземпляра компонента, використовуючи .bind()
конструктор нашого користувацького компонента.
class Foo extends React.Component{ constructor( props ){ super( props ); this.handleClick = this.handleClick.bind(this); } handleClick(event){ // your event handling logic } render(){ return ( Click Me ); } } ReactDOM.render( , document.getElementById("app") );
У цій статті ми збираємося з’ясувати, навіщо нам це робити.
Я б порадив прочитати .bind()
тут, якщо ви ще не знаєте, що це робить.
Звинувачуйте JavaScript, не реагуйте
Що ж, звинувачення звучить трохи жорстко. Це не те, що нам потрібно робити через те, як працює React, або через JSX. Це пов’язано з тим, як this
прив’язка працює в JavaScript.
Давайте подивимося, що трапиться, якщо ми не прив’яжемо метод обробника подій до його екземпляра компонента:
class Foo extends React.Component{ constructor( props ){ super( props ); } handleClick(event){ console.log(this); // 'this' is undefined } render(){ return ( Click Me ); } } ReactDOM.render( , document.getElementById("app") );
Якщо ви запускаєте цей код, натисніть кнопку “Click Me” і перевірте свою консоль. Ви побачите undefined
друк на консолі як значення this
зсередини методу обробки подій. handleClick()
Метод , здається, втратив його контекст (екземпляр компонента) або this
значення.
Як "це" прив'язка працює в JavaScript
Як я вже згадував, це відбувається через те, як this
прив'язка працює в JavaScript. Я не буду вдаватися в багато деталей у цій публікації, але тут є чудовий ресурс, щоб зрозуміти, як this
працює прив’язка в JavaScript.
Але що стосується нашого обговорення тут, значення this
усередині функції залежить від того, як ця функція викликається.
Прив’язка за замовчуванням
function display(){ console.log(this); // 'this' will point to the global object } display();
Це звичайний виклик функції. Значення this
усередині display()
методу в цьому випадку є вікном - або глобальним - об'єктом у несуворому режимі. У суворому режимі this
значення становить undefined
.
Неявна прив'язка
var obj = { name: 'Saurabh', display: function(){ console.log(this.name); // 'this' points to obj } }; obj.display(); // Saurabh
Коли ми викликаємо функцію таким чином - перед цим контекстний об’єкт - this
значення всередині display()
встановлюється на obj
.
Але коли ми призначаємо це посилання на функцію якійсь іншій змінній і викликаємо функцію, використовуючи це нове посилання на функцію, ми отримуємо інше значення this
зсередини display()
.
var name = "uh oh! global"; var outerDisplay = obj.display; outerDisplay(); // uh oh! global
У наведеному вище прикладі, коли ми телефонуємо outerDisplay()
, ми не вказуємо об’єкт контексту. Це звичайний виклик функції без об’єкта власника. У цьому випадку значення this
inside display()
повертається до прив'язки за замовчуванням . Він вказує на глобальний об'єкт або undefined
якщо функція, що викликається, використовує жорсткий режим.
Це особливо застосовно при передачі таких функцій, як зворотні виклики іншій користувацькій функції, сторонній бібліотечній функції або вбудованій функції JavaScript, як setTimeout
.
Розгляньте setTimeout
фіктивне визначення, як показано нижче, а потім застосуйте його.
// A dummy implementation of setTimeout function setTimeout(callback, delay){ //wait for 'delay' milliseconds callback(); } setTimeout( obj.display, 1000 );
Ми можемо зрозуміти, що під час дзвінка setTimeout
JavaScript внутрішньо призначає obj.display
свій аргумент callback
.
callback = obj.display;
Ця операція присвоєння, як ми бачили раніше, призводить display()
до втрати функцією контексту. Коли цей зворотний виклик зрештою викликається всередині setTimeout
, this
значення всередині display()
повертається до прив'язки за замовчуванням .
var name = "uh oh! global"; setTimeout( obj.display, 1000 ); // uh oh! global
Явне тверде прив'язування
Щоб уникнути цього, ми можемо явно важко пов'язатиthis
значення функції, використовуючи bind()
метод.
var name = "uh oh! global"; obj.display = obj.display.bind(obj); var outerDisplay = obj.display; outerDisplay(); // Saurabh
Тепер, коли ми називаємо outerDisplay()
, значення this
вказує obj
всередину display()
.
Навіть якщо ми передамо obj.display
як зворотний виклик, this
значення всередині display()
буде правильно вказувати на obj
.
Відтворення сценарію за допомогою лише JavaScript
На початку цієї статті ми побачили це в нашому під назвою компоненті React Foo
. Якщо ми не прив'язували обробник події з this
, його значення всередині обробника події було встановлено як undefined
.
Як я вже згадував і пояснював, це через те, як this
прив'язка працює в JavaScript, а не пов'язана з тим, як працює React. Тож давайте видалимо специфічний для React код і побудуємо подібний чистий приклад JavaScript для імітації такої поведінки.
class Foo { constructor(name){ this.name = name } display(){ console.log(this.name); } } var foo = new Foo('Saurabh'); foo.display(); // Saurabh // The assignment operation below simulates loss of context // similar to passing the handler as a callback in the actual // React Component var display = foo.display; display(); // TypeError: this is undefined
Ми не імітуємо фактичні події та обробники, а замість цього використовуємо синонімічний код. Як ми спостерігали в прикладі React Component, this
значення полягало undefined
в тому, що контекст був втрачений після передачі обробника як зворотного виклику - синоніму операції присвоєння. Це те, що ми спостерігаємо і в цьому фрагменті JavaScript, який не реагує.
"Почекай хвилинку! Чи не повинно this
значення вказувати на глобальний об'єкт, оскільки ми запускаємо це в несуворому режимі згідно з правилами прив'язки за замовчуванням? " Ви можете запитати.
Ні. Ось чому:
Тіла оголошень класів та вирази класів виконуються в строгому режимі, тобто методах конструктора, статики та прототипу. Функції геттера та сеттера виконуються в суворому режимі.Повну статтю ви можете прочитати тут.
Отже, щоб запобігти помилці, нам потрібно прив’язати this
значення наступним чином:
class Foo { constructor(name){ this.name = name this.display = this.display.bind(this); } display(){ console.log(this.name); } } var foo = new Foo('Saurabh'); foo.display(); // Saurabh var display = foo.display; display(); // Saurabh
Нам не потрібно робити це в конструкторі, і ми можемо зробити це десь ще. Розглянемо це:
class Foo { constructor(name){ this.name = name; } display(){ console.log(this.name); } } var foo = new Foo('Saurabh'); foo.display = foo.display.bind(foo); foo.display(); // Saurabh var display = foo.display; display(); // Saurabh
But the constructor is the most optimal and efficient place to code our event handler bind statements, considering that this is where all the initialization takes place.
Why don’t we need to bind ‘this’
for Arrow functions?
We have two more ways we can define event handlers inside a React component.
- Public Class Fields Syntax(Experimental)
class Foo extends React.Component{ handleClick = () => { console.log(this); } render(){ return ( Click Me ); } } ReactDOM.render( , document.getElementById("app") );
- Arrow function in the callback
class Foo extends React.Component{ handleClick(event){ console.log(this); } render(){ return ( this.handleClick(e)}> Click Me ); } } ReactDOM.render( , document.getElementById("app") );
Both of these use the arrow functions introduced in ES6. When using these alternatives, our event handler is already automatically bound to the component instance, and we do not need to bind it in the constructor.
The reason is that in the case of arrow functions, this
is bound lexically. This means that it uses the context of the enclosing function — or global — scope as its this
value.
In the case of the public class fields syntax example, the arrow function is enclosed inside the Foo
class — or constructor function — so the context is the component instance, which is what we want.
In the case of the arrow function as callback example, the arrow function is enclosed inside the render()
method, which is invoked by React in the context of the component instance. This is why the arrow function will also capture this same context, and the this
value inside it will properly point to the component instance.
For more details regarding lexical this
binding, check out this excellent resource.
To make a long story short
In Class Components in React, when we pass the event handler function reference as a callback like this
Click Me
the event handler method loses its implicitly bound context. When the event occurs and the handler is invoked, the this
value falls back to default binding and is set to undefined
, as class declarations and prototype methods run in strict mode.
When we bind the this
of the event handler to the component instance in the constructor, we can pass it as a callback without worrying about it losing its context.
Arrow functions are exempt from this behavior because they use lexicalthis
binding which automatically binds them to the scope they are defined in.