React基础12 React中的this绑定

React组件中的this绑定到底是怎么一回事?学习一下。

为什么要bind(this)

React的Class组件中常常会用到bind(this)来绑定this,但是究竟为什么要这么做呢?难道Class中的方法拿不到实例的this吗?

来试验一下:

1
2
3
4
5
6
7
8
class Person {
constructor(name) {
this.name = name;
}
say() {
console.log('my name is ' + this.name);
}
}

上面代码的Person类中,say方法去获取了this,然后当我们实例化一个对象时,并调用say()方法时,结果是:

1
2
3
let p1 = new Person('Jay');
p1.say();
// my name is Jay

结果说明在Class的方法中,是可以直接通过this获得实例对象的。所以实际上在React的Class中直接使用this是没有问题的,例如在生命周期函数或者render中。但是在render函数中的JSX模板中的事件处理函数,这里面的调用的方法的this就不会指向组件实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
export default class Demo5 extends Component {
handleClick() {
console.log(this);
}

render() {
return (
<div onClick={this.handleClick}>
Hello
</div>
);
}
}

上面的handleClick函数作为JSX中的事件处理函数,其中的this不会指向组件实例,而是指向了事件响应的上下文环境(非严格环境下是window,严格环境下是undefined),而类声明和类表达式的主体以严格模式执行(主要包括构造函数、静态方法和原型方法以及gettersetter函数),所以this指向undefined

为什么this不能指向组件实例

实际上这与React和JSX语法没有关系,是JavaScript的this绑定机制导致了上述情况的发生。要明确的是,函数内部的this取决于该函数被调用时的上下文环境。

默认绑定

1
2
3
4
5
function display(){
console.log(this); // 'this' 将指向全局变量
}

display();

在上面的情况下,display方法中的this在非严格模式下指向window,在严格模式下指向undefined

隐式绑定

1
2
3
4
5
6
7
8
const obj = {
name: 'Saurabh',
display: function(){
console.log(this.name); // 'this' 指向 obj
}
};

obj.display(); // Saurabh

在上面的情况下,通过obj对象来调用这个函数时,display内部的this指向了obj

但是如果将这个函数赋值给其他变量,并且通过这个变量去调用该函数时,在display中获得this就不同了:

1
2
3
var name = 'hello';
const outer = obj.display;
outer(); // 'hello'

我们调用outer时,并没有指定一个具体的上下文对象,这个时候this值与默认绑定的结果是相同的,在非严格模式下指向window,在严格模式下指向undefined

在将一个方法以回调的形式传递给另外一个函数,或者像setTimeout这样的内置JavaScript函数时,就可以依照上面的过程进行判断

例如我们自定义一个setTimeout方法并调用,预测一下会发生什么:

1
2
3
4
5
6
7
8
//setTimeout 的虚拟实现
function setTimeout(callback, delay){
//等待 'delay' 数个毫秒

callback();
}

setTimeout(obj.display, 1000);

在调用setTimeout时,函数内部将obj.display赋值给参数callback:

1
callback = obj.display;

在一段时间后调用这个方法时,调用的实际上是callback(),而这种调用会让display方法丢失上下文,其中的this会退回至默认绑定,指向全局变量

1
2
3
4
var name = "uh oh! global";
setTimeout( obj.display, 1000 );

// uh oh! global

显示绑定

为了避免上面的情况,可以使用bind来显式的为方法绑定上下文:

1
2
3
4
5
var name = "uh oh! global";
var outerDisplay = obj.display.bind(obj);
outerDisplay();

// Saurabh

绑定了this后,在调用对应的方法也能够渠道我们绑定的上下文。同理,粗若将obj.display作为callback参数传递给函数,display中的this也会正确指向obj

React编译时的处理

明确了隐式绑定时,将方法作为参数传递给另一个函数时会导致该方法的上下文丢失。而在React的类的写法中,JSX的事件处理程序的this会丢失,就是因为这个原因。

在编译JSX的过程中,事件处理程序会作为属性值被放置在一个对象中,调用时会识别为函数调用模式,上下文丢失。

举个例子,下面的Class组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default class Demo extends Component {
handleClick() {
console.log(this);
}

render() {
this.handleClick();

return (
<div onClick={this.handleClick}>
Hello
</div>
);
}
}

编译完是下面这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const Demo = function (_Component) {
_inherits(Deom, _Component);
function Demo() {
_classCallCheck(this, Demo);
return _possibleConstructorReturn(this, _Component.apply(this, arguments));
}
Demo.prototype.handleClick = function handleClick() {
console.log(this);
}
Demo.prototype.render = function render() {
this.handleClick();
return __WEBPACK_IMPORTED_MODULE_0_react___default.a.createElement(
'div',
{ onClick: this.handleClick },
'hello',
)
}
return Demo;
}(__WEBPACK_IMPORTED_MODULE_0_react__["Component"])

很明显的看到,当编译完成后,JSX中的事件处理程序handleClick是放置在{ onClick: this.handleClick }中,当点击事件触发时实际上调用的是onClick,和隐式调用中的结果一样,上下文丢失了,this指向undefined

而如果我们在JSX中使用bind绑定this

1
<div onClick={this.handleClick.bind(this)}>Hello</div>

那么编译后就变成了{ onClick: this.handleClick.bind(this) },这就显式的绑定了this

解决方法

(1)React官方首推的一种方法就是使用实验性的语法public class fileds,可以使用class fields正确的绑定回调函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
export default class Demo extends Component {
handleClick = () => {
console.log(this);
};

render() {
return (
<div onClick={this.handleClick}>
Hello
</div>
);
}
}

上面的写法会会保证handleClick内的this被正确绑定,要注意的是这是一个实验性的语法,但是Create React App默认启用了这个语法。

如果没有使用Create React App的话需要手动开启这个语法,使用Babel的transform-class-properties 或者enable stage-2 in Babel这两项功能

(2)第种方式是在JSX的回调函数中使用箭头函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
export default class Demo extends Component {
handleClick() {
console.log(this);
}

render() {
return (
<div onClick={(e) => this.handleClick(e)}>
Hello
</div>
);
}
}

这种语法的主要问题还是来自于性能的担忧,因为每次渲染组件时都会创建不同的回调函数。大多数情况时没有什么问题,但是如果该回调函数作为prop传入子组件时,这些组件可能会进行额外的重新渲染。

(3)第三种方式是在JSX的回调函数中使用bind进行绑定:

1
2
3
4
5
6
7
8
9
10
11
12
13
export default class Demo extends Component {
handleClick() {
console.log(this);
}

render() {
return (
<div onClick={this.handleClick.bind(this)}>
Hello
</div>
);
}
}

这种方法的问题和上一种是类似的,有可能带来性能的问题。

(4)第四种方式是在构造函数constructor中进行显示的绑定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export default class Demo extends Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}

handleClick() {
console.log(this);
}

render() {
return (
<div onClick={this.handleClick}>
Hello
</div>
);
}
}

这样就可以避免第二种方法可能带来的性能问题,而且很直观,但问题是增加了代码数量,而且为了绑定this必须声明constructor,在constructor中还不能忘记super,有点麻烦

所以,从性能角度上考虑,如果开启了对应的public class fileds语法(使用了Create React App),那么建议使用第一种方式,否则的话建议使用最后一种方式。

参考