[转载|译] 为什么需要在 React 类组件中为事件处理程序绑定 this

阿凡达
• 阅读 3013

在使用 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

好吧,责怪听起来有些苛刻。如果按照 ReactJSX 的语法,我们并不需要这么做。其实绑定 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 在非严格模式下指向 windowglobal 对象。在严格模式下,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 (node)

在上面的例子里,当我们调用 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() 方法绑定到函数上。

bind方法返回一个新函数,其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")
);

// 注意 handleClick写法:
// handleClick = () => {
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 绑定,会将其自动绑定到定义他们的函数上下文。

点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
7个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Jacquelyn38 Jacquelyn38
4年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
React组件中的函数绑定this的几种方式
在React中使用class定义组件时如果不注意this的指向问题,会带来一些麻烦。绑定this主要有下面两种方法:1\.bind()在class中定义函数,然后在构造方法中使用bind()绑定当前的组件对象。classMyComponentextendsReact.Component{constr
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
为什么mysql不推荐使用雪花ID作为主键
作者:毛辰飞背景在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建议采用uuid,使用uuid究
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
阿凡达
阿凡达
Lv1
攒满想念,期待下次再见。
文章
3
粉丝
0
获赞
0