React 组件中处理 onClick 类似事件绑定的时候,是需要显式给处理器绑定上下文(context)的,这一度使代码变得冗余和难看。

请看如下的示例:

class App extends Component {
constructor() {
super();
this.state = {
isChecked: false
};
}
render() {
return (
  <span class="pl-k">&lt;</span>div className<span class="pl-k">=</span><span class="pl-s"><span class="pl-pds">"</span>App<span class="pl-pds">"</span></span><span class="pl-k">&gt;</span>
<span class="pl-k">&lt;</span>label <span class="pl-k">&gt;</span>
check me<span class="pl-k">:</span>
<span class="pl-k">&lt;</span>input
type<span class="pl-k">=</span><span class="pl-s"><span class="pl-pds">"</span>checkbox<span class="pl-pds">"</span></span>
checked<span class="pl-k">=</span>{<span class="pl-c1">this</span>.<span class="pl-smi">state</span>.<span class="pl-smi">isChecked</span>}
onChange<span class="pl-k">=</span>{<span class="pl-c1">this</span>.<span class="pl-smi">toggleCheck</span>}
<span class="pl-k">/</span><span class="pl-k">&gt;</span>
<span class="pl-k">&lt;</span><span class="pl-k">/</span>label<span class="pl-k">&gt;</span>
<span class="pl-k">&lt;</span><span class="pl-k">/</span>div<span class="pl-k">&gt;</span>
);

}

toggleCheck() {

this.setState(currentState => {

return {

isChecked: !currentState.isChecked

};

});

}

}

页面上放了一个 checkbox 元素,点击之后切换其选中状态。这是很直观的一段代码,但并不会像你想的那样正常工作。



事件处理器上下文丢失的报错

因为 checkboxonChange 事件处理器中,找不到 React 组件的 setState 方法,这说明其执行时的上下文不是该组件,而是别的什么东西,具体我们来调试下。



调试查看丢失上下文后 this 的值

出乎意料,是 undefined,这个方法在一个完全野生的环境下执行,没有任何上下文。

WHY

当然这并不是 React 的锅,这是 JavaScript 中 this 的工作原理。具体可参见 Chapter 2: this All Makes Sense Now! 来追溯其底层原因,简单来讲 this 的值取决于函数调用的方式。

默认的绑定

function display(){
  console.log(this)
} display() // 严格模式下为全局 window,非严格模式下为 undefined

隐式绑定

通过对象来调用,该函数的上下文被隐式地指定为该对象。

var obj = {
name: 'Nobody',
display: function(){
console.log(this.name);
}
};
obj.display(); // Nobody. 里面取的是 obj 身上的 `name` 属性

但,如果把该对象上的方法赋值给其他变量,或通过参数传递的形式,再执行,那光景就又不一样了。

var obj = {
name: "Nobody",
display: function() {
console.log(this.name);
}
}; var name = "global!";

var outerDisplay = obj.display;

outerDisplay(); // global! 这里取到的 name 是全局中的内个

这里赋值给 outerDisplay 后再调用,等同于调用一个普通函数,而不是对象中的那个,所以此时 this 为全局对象,刚好全局里面有定义一个 name 变量。同样地,如果是严格模式下,因为此时 thisundefined,所以访问不到所谓的 undefiend.name,于是会抛错。

function invoker(fn) {
fn();
} setTimeout( obj.display, 1000 ); // global!

invoker(obj.display); // global!

这里 setTimeout 调用的时候,因为它的签名实际上是 setTimeout(fn,delay),所以,可以理解为将 obj.display 赋值给了它的入参 fn,实际上执行的是 fn 而不再是对象上的方法了。对于 invoker 函数也是一样的道理。

强制绑定

这个时候,bind 就成了那个拯救世界的英雄,任何时间我们都可以通过它来显式地指定函数的执行上下文。

var name = “global!”;
obj.display = obj.display.bind(obj);
var outerDisplay = obj.display;
outerDisplay(); // Nobody

bind 将指定的上下文与函数绑定后返回一个新的函数,这个新函数再拿去赋值或传参什么的都不会对其上下文产生影响了,执行时始终是我们指定的那个。

现场还原

有了上面的背景,就可以还原文章开头的问题了,即事件处理器的上下文 丢失的问题。

JSX 中的 HTML 标签本质上对应 React 中创建该标签的一个函数。比如你写的 div 编译会其实是 React.createElement(‘div’)。所以当你书写 <Input> 时其实是调用了 React.createElement 来创建一个 <Input> 标签。

React.createElement(
type,
[props],
[...children]
)

标签上的属性会作为 props 参数传递给 createElement 函数。

<Input onChange={this.toggleCheck}> 表示将组件中的 toggleCheck 方法赋值给 createElement 的入参 propsprops 是个对象,接收所有书写在标签上的属性,),实际调用的时候一如上面所说的,调用的已经不是组件中的 toggleCheck 方法了。

React.createElement(type, props){
// 让我们创建一个 <type> 并在 <type> 的值发生变化的时候调用一下 `props.onChange`
...
props.onChange() // 它已经不是原来的方法了,丢失了上下文
...
}

因为 ES6 的 Class 是在严格模式下执行的,所以事件处理器中如果使用了 this 那它就是 undefined

所以你看到 React 官方的示例中,constructor 里有 bind(this) 的语句就不奇怪了,就是为了纠正这个事件处理器歪了的执行上下文。

  constructor() {
super();
this.state = {
isChecked: false
};
+ this.toggleCheck = this.toggleCheck.bind(this);
}

这样是能正常工作了,但是,这句代码的存在真的很别扭,因为,

  • 对于业务来说,毫无意义,徒增代码量
  • 很丑陋,每加一个处理器就要加一条这样的绑定
  • 冗余,这样重复的代码大量冗余在项目中,在搜索中混淆了原本的方法

避免的方式有很多,就看哪种最对味。下面来看看如何避免写这些绑定方法。

#0行内的绑定

最简单的可以在行内进行绑定操作,这样不用单独写一句出来。

   <input
type="checkbox"
checked={this.state.isChecked}
- onChange={this.toggleCheck}
+ onChange={this.toggleCheck.bind(this)}
/>

#1箭头函数

因为箭头函数不会创建新的作用域,其上下文是语义上的(lexically)上下文。所以在绑定事件处理器时,直接使用剪头函数是很方便的一种规避方法。

          <input
type="checkbox"
checked={this.state.isChecked}
- onChange={this.toggleCheck}
+ onChange={() => this.toggleCheck()}
/>

#2将类的方法改成属性

如果将这个处理器作为该组件的一个属性,这个属性作为事件的处理器以箭头函数的形式存在,执行的时候也是能正常获取到上下文的。

-  toggleCheck() {
+ toggleCheck = () => {
this.setState(currentState => {
return {
isChecked: !currentState.isChecked
};
});
}

总结

React 组件中,其实跟 React 没多大关系,传递事件处理器,或方法作为回调时,其上下文会丢失。为了修复,我们需要显式地给这个方法绑定一下上下文。除了常用的在构造器中进行外,还可通过箭头函数,公有属性等方式来避免冗余的绑定语句。

相关资源

规避 React 组件中的 bind(this)的更多相关文章

  1. 【React】282- 在 React 组件中使用 Refs 指南

    英文:Yomi Eluwande  译文:joking_zhang https://segmentfault.com/a/1190000019277029 使用 React 时,我们的默认思维方式应该 ...

  2. 在 React 组件中使用 Refs 指南

    原文:Fullstack React's Guide to using Refs in React Components作者:Yomi Eluwande译者:博轩 译文:https://segment ...

  3. react组件中的constructor和super小知识

    react组件中的constructor和super小知识 1.react中用class申明的类一些小知识 如上图:类Child是通过class关键字申明,并且继承于类React. A.Child的类 ...

  4. React组件中的key

    React组件中的key 一.key的作用 react中的key属性,它是一个特殊的属性,它是出现不是给开发者用的(例如你为一个组件设置key之后不能获取组件的这个key props),而是给reac ...

  5. React组件中对子组件children进行加强

    React组件中对子组件children进行加强 问题 如何对组件的children进行加强,如:添加属性.绑定事件,而不是使用<div>{this.props.children}< ...

  6. 优雅的在React组件中注册事件

    前言 在React的开发中,我们经常需要在 window 上注册一些事件, 比如按下 Esc 关闭弹窗, 按上下键选中列表内容等等.比较常见的操作是在组件 mount 的时候去 window 上监听一 ...

  7. 在 React 组件中监听 android 手机物理返回/回退/back键事件

    当前端页面嵌入到 webview 中运行时,有时会需要监听手机的物理返回按键事件来做一些自定义的操作. 比如我最近遇到的,在一个页面里面有批量选择的功能,当点击手机的返回键时,清除页面上的选中状态.我 ...

  8. react组件中返回并列元素的方法

    我们在写react组件的时候,经常会遇到这种问题,在render中return元素只能有一个顶级元素,比如div,假如写成这样就会报错: render(){ return( <div>12 ...

  9. react组件中刷新组件小技巧

    在开发过程中,经常遇到组件数据无法更新,例如:当你用同一个表格展示不同数据的时候,当点击第5页后,再点击另外一份数据时发现还在第五页,并没有回到第一页. 怎么能让一个组件每次数据不一样时都重新加载呢, ...

随机推荐

  1. 微信跳转,wap网页跳转微信打开指定页面

    最近一朋友的客户有这么一需求,wap网页跳转微信打开指定页面,让用户去关注公众号.想这么干,通过网页跳转到微信并打开带有二维码的网页,用户长按识别二维码就可以关注了.想法很好,于是去搜索了“微信跳转, ...

  2. react 的进阶

    一 react 中table报错 validateDOMNesting(...): <tr> cannot appear as a child of <table>. See ...

  3. error C2371: 'IServiceProvider' : redefinition; different basic types

    需要将#include <windows.h>放到using namespace System;前面. #include <windows.h>using namespace ...

  4. System.data.sqlclient.sqlexception:将截断字符串或二进制数据终止

    System.data.sqlclient.sqlexception:将截断字符串或二进制数据终止. 错误原因:输入的字符串长度超过数据库设置的长度

  5. Linux用命令启动程序(eclipse、IDEA等)

    打开根目录用管理员权限打开HOME 找到下图截图中的框选出的文件 用文本编辑器打开后 在文件末尾添加所需要打开的应用文件所在的目录 这里以本人的IDEA和eclipse为例:

  6. 【ASP】session实现购物车

    1.问题提出 利用session内置对象,设计并实现一个简易的购物车,要求如下: 1)利用用户名和密码,登录进入购物车首页 2)购物首页显示登录的用户名以及该用户是第几位访客.(同一用户的刷新应该记录 ...

  7. Spring整合MybatisPlus学习笔记

    简介 MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发.提高效率而生 特性 无侵入:只做增强不做改变,引入它不会对现有 ...

  8. 编译的时候出现 usr/bin/ld: cannot find -lxxx 的错误

    比如错误为: usr/bin/ld: cannot find -labc 首先要明白的是 labc 代表的是 libabc.so 明白了这个之后,我们就知道了问题是:在编译的过程中,需要 libabc ...

  9. 神经网络_线性神经网络 2 (Nerual Network_Linear Nerual Network 2)

    1 LMS 学习规则 1.1 LMS学习规则定义 MSE=(1/Q)*Σe2k=(1/Q)*Σ(tk-ak)2,k=1,2,...,Q 式中:Q是训练样本:t(k)是神经元的期望输出:a(k)是神经元 ...

  10. win7下配置mysql的my.ini文件

    一.环境 操作系统是win7 x64, mysql是5.6.40. 二. 怎么配置? 修改my.ini文件, 添加[client], 在下面加一行 default-character-set=utf8 ...