
בזמן העבודה על 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 lexicalthis
binding which automatically binds them to the scope they are defined in.