זו הסיבה שאנחנו צריכים לאגד מטפלים באירועים ברכיבי כיתה בתגובה

בזמן העבודה על 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") );

אם אתה מפעיל קוד זה, לחץ על כפתור "לחץ עלי" ובדוק את המסוף שלך. תראה 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הפנימי 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 בלבד

בתחילת מאמר זה ראינו זאת ברכיב התגובה שלנו שנקרא Foo. אם לא קשרנו את מטפל האירועים איתו this, ערכו בתוך מטפל האירועים הוגדר כ- undefined.

כפי שציינתי והסברתי, זה בגלל האופן שבו thisהכריכה פועלת ב- JavaScript ולא קשורה לאופן שבו 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

איננו מדמים אירועים ומטפלים בפועל, אלא במקום זאת אנו משתמשים בקוד נרדף. כפי שציינו בדוגמה של רכיב תגובה, thisהערך היה undefinedכאשר ההקשר אבד לאחר העברת המטפל כקריאה להתקשרות - שם נרדף לפעולת הקצאה. זה מה שאנו צופים כאן גם בקטע זה של JavaScript שאינו מגיב.

"חכה דקה! האם thisהערך לא אמור להצביע על האובייקט הגלובלי, מכיוון שאנו מריצים זאת במצב לא קפדני על פי כללי איגוד ברירת המחדל? " אולי תשאל.

לא. זו הסיבה:

גופות הצהרות בכיתה ו ביטויים בכיתה מבוצעות במצב קפדן, כי הוא בנאי, שיטות סטטיות אב טיפוס. פונקציות Getter ו- Setter מבוצעות במצב קפדני.

תוכלו לקרוא את המאמר המלא כאן.

לכן, כדי למנוע את השגיאה, עלינו לאגד את 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 lexicalthisbinding which automatically binds them to the scope they are defined in.