这是我参与「第四届青训营 」笔记创作活动的第5天
初学React时,往往会被组件中的事件回调函数中的this指向问题搞得头晕。本文是我的一些自言自语,希望自己能给自己讲清楚这个问题。
问题分析
故事的开始要从这个错误说起:
无法从undefined中读取值?
来看看源代码:
class Toggle extends React.Component{
constructor (props) {
super(props);
this.state = {isOn: true};
}
render() {
return (
<h1 onClick={this.switch}>{this.state.isOn ? 'On' : 'OFF'}</h1>
)
}
switch () {
// 通过上面的报错,问题显然出在这里!
this.setState((prevState) => {
return {isOn : ! prevState.isOn};
});
}
}
ReactDOM.render(<Toggle/>, document.querySelector('.test'));
定义了一个Toggle组件,当单击h1时,会调用一个事件回调函数switch用于取反state中isOn属性的值,这样h1中的文字就会随着鼠标的点击而变化。
通过报错定位问题,显然问题出在第13行的this.setState()上面,报错显示不能从undefined中读取数据,这就是在告诉我们这里的this是undefined。
冷静分析一下,switch是Toggle类中的一个方法,那它里面的this就应该指向它的实例化对象,此时的this就不应该是undefined。 但是,代码中的switch真的是通过实例化对象调用的吗?好像不是吧~
案例中,switch方法只是被作为一个表达式传入了h1的onClick={}中,也就是说它是作为h1点击事件的回调函数,并没有被Toggle的实例对象调用,这也就导致switch中的值会在它真正被调用的时候确定。
按照这个思路,switch会在h1被点击的时候调用,此时this也会被定义,那这样的话,this更应该指向windows才对呀。 这么想其实是正确的,但只在JS的非严格模式中正确。在严格模式中,this禁止指向全局(windows), 指向windows的this一律为变为undefined。而Babel在转换我们的代码时,会开启严格模式,这也就解释清了为什么switch中的this会变为undefined。
解决方法
1. 使用bind()绑定this指向
既然我们不想this因为被谁调用,在哪里调用的问题而改变,所以不如使用bind()方法来绑定this的指向。
具体代码如下:
...
constructor (props) {
super(props);
this.state = {isOn: true};
this.switch = this.switch.bind(this); // 此时的this就是Toggle
}
...
在类中使用方法名(){}创建的方法会被添加的这个类的原型中(Prototype),那这个语句的操作就是:在Toggle类中新建一个方法,这个方法也叫switch,它其实就是经过绑定this后的原型类中的switch。 这样来看,Toggle对象中应该有一个switch,并且在它的原型中还有一个switch。
不出所料,果然是这样。
这种方法并不是最佳实践。首先,它并不直观,方法代码块和修改this指向的代码被分离了。其次,一次只能绑定一个事件函数,那如果你有10个事件函数呢?
2. 使用箭头函数定义方法
箭头函数是ES6的新特性,它的特点是自身没有this,它的this由上下文决定。既然没有this,也就不用担心this会被改变的问题。并且方法都定义在类中,那用箭头函数定义的方法的上下文其实就是这个类的内部,那么这个this也就指向这个类。
...
switch = () => {
this.setState((prevState) => {
return {isOn: !prevState.isOn};
})
}
render() {
return (
<h1 onClick={this.switch}>{this.state.isOn ? 'On' : 'OFF'}</h1>
)
}
...
或者,也可以这么写:
...
switch () {
this.setState((prevState) => {
return {isOn : ! prevState.isOn};
});
}
render() {
return (
<h1 onClick={() => this.switch()}>{this.state.isOn ? 'On' : 'OFF'}</h1>
)
}
...
第二种方式的缺点是,每一次调用都会新生成一个回调函数,所以更推荐使用第一种。
最佳实践
解决上面的问题,并优化代码,看看最终的结果:
class Toggle extends React.Component {
state = {isOn: true};
render() {
return (
<h1 onClick={ this.switch }>{ this.state.isOn ? "On" : "OFF" }</h1>
);
}
switch = () => {
this.setState((prevState) => ({isOn: !prevState.isOn}));
}
}
ReactDOM.render(<Toggle />, document.querySelector(".test"));
推荐阅读:
\