天天看点

为什么在 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 绑定,该绑定自动将它们绑定到它们定义的范围。

继续阅读