天天看點

為什麼在 React 的 Class 元件中綁定事件處理程式

作者:迹憶客

在使用 React 時,我們一定遇到過控制元件和事件處理程式。 我們需要在自定義元件的構造函數中使用 .bind() 将這些方法綁定到元件執行個體上。如下代碼所示:

class Foo extends React.Component{
  constructor( props ){
    super( props );
    this.handleClick = this.handleClick.bind(this);
  }
  
  handleClick(event){
    // 事件處理程式
  }
  
  render(){
    return (
      <button type="button" 
      onClick={this.handleClick}>
      Click Me
      </button>
    );
  }
}

ReactDOM.render(
  <Foo />,
  document.getElementById("app")
);           

在本文中,我們将介紹為什麼我們需要這樣做。

Javascript 還是 React

首先我們要怪Javascript 而不是 React。由于 React 的工作方式或 JSX,這不是我們需要做的事情。 這是因為 this 綁定是 JavaScript 中的工作方式。

讓我們看看如果我們不将事件處理程式方法與其元件執行個體進行綁定會發生什麼:

class Foo extends React.Component{
  constructor( props ){
    super( props );
  }
    
  handleClick(event){
    console.log(this); // 'this' 結果為 undefined
  }
    
  render(){
    return (
      <button type="button" onClick={this.handleClick}>
        Click Me
      </button>
    );
  }
}

ReactDOM.render(
  <Foo />,
  document.getElementById("app")
);           

如果運作此代碼,我們将看到在事件處理程式方法内部列印出來的 this 的值為 undefined 。 handleClick() 方法似乎丢失了它的上下文(元件執行個體)或這個值。

JavaScript 中 “this”綁定的工作原理

正如所提到的,發生這種情況是因為這種綁定在 JavaScript 中的工作方式。 這篇文章中不會對此進行詳細介紹,但是我們會在後續的文章中來介紹JavaScript中 this 綁定是如何工作的。

但與我們在這裡的讨論相關的是,函數内部 this 的值取決于該函數的調用方式。

預設綁定

function display(){
 console.log(this); // 'this' 隻想全局對象 object
}

display();            

這是一個普通的函數調用。 在這種情況下,display() 方法中 this 的值是非嚴格模式下的 window 或全局對象 object。 在嚴格模式下, this 值是未定義的。

隐式綁定

var obj = {
 name: '迹憶客',
 display: function(){
   console.log(this.name); // 'this' 指向 obj
  }
};

obj.display(); // 迹憶客            

當我們以這種方式調用函數時——前面有一個上下文對象——display() 中的 this 值被設定為 obj。

但是當我們将這個函數引用配置設定給其他變量并使用這個新的函數引用調用函數時,我們在 display() 中得到了不同的 this 值。

var name = "uh oh! global";
var outerDisplay = obj.display;
outerDisplay(); // uh oh! global           

在上面的例子中,當我們調用 outerDisplay() 時,我們沒有指定上下文對象。 這是一個沒有所有者對象的普通函數調用。 在這種情況下,display() 内部的 this 的值回退到預設綁定。 如果被調用的函數使用嚴格模式,則它指向全局對象或是未定義。

這在将回調等函數傳遞給另一個自定義函數、第三方庫函數或内置 JavaScript 函數(如 setTimeout)時尤其适用。

考慮如下所示的 setTimeout 定義,然後調用它。

// setTimeout 的模拟實作
function setTimeout(callback, delay){

   //等待 'delay' 毫秒
   callback();
   
}

setTimeout( obj.display, 1000 );           

我們可以發現,當我們調用 setTimeout 時,JavaScript 在内部将 obj.display 配置設定給它的參數 callback 。

callback = obj.display;           

正如我們之前看到的,這個指派操作會導緻 display() 函數失去它的上下文。 當最終在 setTimeout 中調用此回調函數時,display() 中的 this 值将回退到預設綁定。

var name = "uh oh! global";
setTimeout( obj.display, 1000 );

// uh oh! global           

顯式綁定

為了避免這種情況,我們可以使用 bind() 方法将 this 值顯式地綁定到一個函數。

var name = "uh oh! global";
obj.display = obj.display.bind(obj); 
var outerDisplay = obj.display;
outerDisplay();

// 迹憶客           

現在,當我們調用 outerDisplay() 時, this 的值指向 display() 内部的 obj。

即使我們将 obj.display 作為回調傳遞,display() 中的 this 值也會正确指向 obj 。

在本文開頭,我們在名為 Foo 的 React 元件中看到了這一點。 如果我們沒有用 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

// 下面的指派操作模拟了上下文丢失,類似于在實際 React 元件中将處理程式作為回調傳遞
var display = foo.display; 
display(); // TypeError: this is undefined           

我們沒有模拟實際的事件和處理程式,而是使用同義代碼。 正如我們在 React 元件示例中觀看到的, this 值未定義,因為在将處理程式作為回調傳遞後上下文丢失了——與指派操作同義。 這也是我們在這個非 React JavaScript 片段中觀察到的。

你可能會問:“等一下! this 值不應該指向全局對象嗎,因為我們是按照預設綁定規則在非嚴格模式下運作 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('迹憶客');
foo.display(); // 迹憶客

var display = foo.display;
display(); // 迹憶客           

上面代碼運作結果如下

為什麼在 React 的 Class 元件中綁定事件處理程式

綁定this執行結果

我們不需要在構造函數中這樣做,我們也可以在其他地方這樣做。 考慮一下:

class Foo {
  constructor(name){
    this.name = name;
  }
  display(){
    console.log(this.name);
  }
}

var foo = new Foo('迹憶客');
foo.display = foo.display.bind(foo);
foo.display(); // 迹憶客

var display = foo.display;
display(); // 迹憶客           

但是構造函數是編寫我們的事件處理程式綁定語句的最佳和最有效的地方,因為這是進行所有初始化的地方。

為什麼我們不需要為箭頭函數綁定“this”?

我們還有另外兩種方法可以在 React 元件中定義事件處理程式。

  • 公共類字段文法(實驗性)
class Foo extends React.Component{
handleClick = () => {
  console.log(this); 
}
render(){
  return (
    <button type="button" onClick={this.handleClick}>
      Click Me
    </button>
  );
}
} 
ReactDOM.render(
<Foo />,
document.getElementById("app")
);           
  • 回調中的箭頭函數
class Foo extends React.Component{
handleClick(event){
  console.log(this);
} 
render(){
  return (
    <button type="button" onClick={(e) => this.handleClick(e)}>
      Click Me
    </button>
  );
}
}
ReactDOM.render(
<Foo />,
document.getElementById("app")
);           

這兩個都使用了 ES6 中引入的箭頭函數。使用這些替代方案時,我們的事件處理程式已經自動綁定到元件執行個體,我們不需要在構造函數中綁定它。

原因是在箭頭函數的情況下, this 是詞法綁定的。這意味着它使用封閉函數(或全局)範圍的上下文作為其 this 值。

在公共類字段文法示例的情況下,箭頭函數包含在 Foo 類或構造函數中,是以上下文是元件執行個體,這就是我們想要的。

在将箭頭函數作為回調示例的情況下,箭頭函數包含在 render() 方法中,該方法由 React 在元件執行個體的上下文中調用。這就是為什麼箭頭函數也會捕獲相同的上下文,并且其中的 this 值将正确地指向元件執行個體。

總結

在 React 的類元件中,當我們像這樣将事件處理函數引用作為回調傳遞時

<button type="button" onClick={this.handleClick}>Click Me</button>           

事件處理程式方法丢失其隐式綁定的上下文。 當事件發生并調用處理程式時, this 值回退到預設綁定并設定為 undefined ,因為類聲明和原型方法在嚴格模式下運作。

當我們将事件處理程式的 this 綁定到構造函數中的元件執行個體時,我們可以将其作為回調傳遞,而不必擔心它會丢失其上下文。

箭頭函數不受這種行為的影響,因為它們使用詞法 this 綁定,該綁定自動将它們綁定到它們定義的範圍。

繼續閱讀