[譯] 為什麼需要在 React 類元件中為事件處理程式繫結 this

saku發表於2018-05-15

[譯] 為什麼需要在 React 類元件中為事件處理程式繫結 this

背景圖源來自 Kaley Dykstra 併發布在 Unsplash 上,原始碼影象生成自 carbon.now.sh

在使用 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")
);
複製程式碼

在這篇文章中,我們將探究為什麼要這麼做。

如果你對 .bind() 尚不瞭解,推薦閱讀 這篇文章

應責怪 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")
);
複製程式碼

如果你執行這個程式碼,點選 “Click Me” 按鈕,檢查你的控制檯,你將會看到控制檯列印出 undefined,這個值是 handleClick() 方法內部的 this 值。handleClick() 方法似乎已經丟失了其上下文(元件例項),即 this 值。

在 JavaScript 中,this 的繫結是如何工作的

正如我上文提到的,是 JavaScript 的 this 繫結機制導致了上述情況的發生。在這篇文章中,我不會深入探討太多細節,但是 這篇文章 可以幫助你進一步學習在 JavaScript 中 this 的繫結是如何工作的。

與我們討論相關的是,函式內部的 this 的值取決於該函式如何被呼叫。

預設繫結

function display(){
 console.log(this); // 'this' 將指向全域性變數
}

display(); 
複製程式碼

這是一個普通的函式呼叫。在這種情況下,display() 方法中的 this 在非嚴格模式下指向 window 或 global 物件。在嚴格模式下,this 指向 undefined

隱式繫結

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

obj.display(); // Saurabh 
複製程式碼

當我們以一個 obj 物件來呼叫這個函式時,display() 方法內部的 this 指向 obj

但是,當我們將這個函式引用賦值給某個其他變數並使用這個新的函式引用去呼叫該函式時,我們在 display() 中獲得了不同的this值。

var name = "uh oh! global";
var outerDisplay = obj.display;
outerDisplay(); // uh oh! global
複製程式碼

在上面的例子裡,當我們呼叫 outerDisplay() 時,我們沒有指定一個具體的上下文物件。這是一個沒有所有者物件的純函式呼叫。在這種情況下,display() 內部的 this 值回退到預設繫結。現在這個 this 指向全域性物件,在嚴格模式下,它指向 undefined

在將這些函式以回撥的形式傳遞給另一個自定義函式、第三方庫函式或者像 setTimeout 這樣的內建JavaScript函式時,上面提到的判斷方法會特別實用。

考慮下方的程式碼,當自定義一個 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
複製程式碼

明確繫結

為了避免這種情況,我們可以使用 明確繫結方法,將 this 的值通過 bind() 方法繫結到函式上。

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

// Saurabh
複製程式碼

現在,當我們呼叫 outerDisplay() 時,this 的值指向 display() 內部的 obj

即時我們將 obj.display 直接作為 callback 引數傳遞給函式,display() 內部的 this 也會正確地指向 obj

僅使用 JavaScript 重新建立場景

在本文的開頭,我們建立了一個類名為 Foo 的 React 元件。如果我們不將 this 繫結到事件上,事件內的值會變成 undefined

正如我上文解釋的那樣,這是由 JavaScript 中 this 繫結的方式決定的,與React的工作方式無關。因此,讓我們刪除 React 本身的程式碼,並構建一個類似的純 JavaScript 示例,來模擬此行為。

class Foo {
  constructor(name){
    this.name = name
  }

  display(){
    console.log(this.name);
  }
}

var foo = new Foo('Saurabh');
foo.display(); // Saurabh

//下面的賦值操作模擬了上下文的丟失。 
//與實際在 React Component 中將處理程式作為 callback 引數傳遞相似。
var display = foo.display; 
display(); // TypeError: this is undefined
複製程式碼

我們不是模擬實際的事件和處理程式,而是用同義程式碼替代。正如我們在 React 元件示例中所看到的那樣,由於將處理程式作為回撥傳遞後,丟失了上下文,導致 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
複製程式碼

但由於建構函式是所有初始化發生的地方,因此它是編寫繫結事件語句最佳的位置。

為什麼我們不需要為箭頭函式繫結 ‘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 值將正確的指向元件例項。

有關 this 繫結的更多細節,請檢視 此優秀資源

總結

在 React 的類元件中,當我們把事件處理函式引用作為回撥傳遞過去,如下所示:

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

複製程式碼

事件處理程式方法會丟失其隱式繫結的上下文。當事件被觸發並且處理程式被呼叫時,this的值會回退到預設繫結,即值為 undefined,這是因為類宣告和原型方法是以嚴格模式執行。

當我們將事件處理程式的 this 繫結到建構函式中的元件例項時,我們可以將它作為回撥傳遞,而不用擔心會丟失它的上下文。

箭頭函式可以免除這種行為,因為它使用的是詞法 this 繫結,會將其自動繫結到定義他們的函式上下文。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章