0%

我们为什么要在React中使用bind()

最近在复盘代码的过程中思考了这个问题,顺便复习bind()的用法和JavaScript中this指向的问题

在我们使用React进行开发的时候,我们必须使用.bind(this)方法在组件的构造函数中把事件处理函数绑定到组价实例上,例如这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Foo extends React.Component{
constructor( props ){
super( props );
this.handleClick = this.handleClick.bind(this);
}

handleClick(event){
// your event handling logic
}

render(){
return (
<button type="button"
onClick={this.handleClick}>
Click Me
</button>
);
}
}

ReactDOM.render(
<Foo />,
document.getElementById("app")
);

这篇文章中,我们将深入探究其中的原因。

在这之前,如果你对.bind()函数不了解的话,你可以阅读这篇文档

这其实是JavaScript而不是React的锅

其实这个问题不是React或者是JSX的锅,是我们在使用JavaScript中的this指向问题。

让我们看一下如果我们不在组件实例中绑定this的话,会发生什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

class Foo extends React.Component{
constructor( props ){
super( props );
}

handleClick(event){
console.log(this); // 'this' is undefined
}

render(){
return (
<button type="button" onClick={this.handleClick}>
Click Me
</button>
);
}
}

ReactDOM.render(
<Foo />,
document.getElementById("app")
);

在上面的代码中,我们为button绑定了一个onclick事件,然后打印出当前的this值。如果你把代码跑起来,点击click me按钮然后打开你的控制台你就能发现,打印出来的是undefinedhandleClick()函数看起来就“丢失”了他的上下文,也就是组件实例

JavaScript中this的指向问题

老生常谈了,就跟我们上面所说的一样这是JavaScript中的this绑定问题。这篇文章中我们仅会介绍与React相关的,不会深入到具体细节

this的取值,取决于函数的调用!
this的取值,取决于函数的调用!
this的取值,取决于函数的调用!

默认绑定 Default Binding

1
2
3
4
function display(){
console.log(this); // 'this' will point to the global object
}
display();

这就是一个简单的函数调用,在这种情况下dispaly()函数中的this的值指向的是window全局对象——或者是global全局对象,仅仅是在非严格模式下non strict mode。在严格模式下的话,this的值是undefined

Implicit Binding 隐式绑定

1
2
3
4
5
6
7
var obj = {
name: 'Saurabh',
display: function(){
console.log(this.name); // 'this' points to obj
}
};
obj.display(); // Saurabh

当我们在这么调用一个函数时候——即在一个对象的上下文(a context object)中调用函数——display()this的值就被绑定到了obj这个对象上

但是呢,当我们把这个函数的引用赋值给其他变量,然后我们就得到了一个新的函数引用(a new function reference)听起来真奇怪…这样我们在调用这个新的函数引用时,display()this就发生了改变

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

在上面的示例中,当我们调用outerDisplay()的时候,我们没有为它指定一个对象上下文(context object)。所以他就变成了一个简单的函数调用,而不是上面那样在对象里的调用。在这种情况下,display()this的值就变成了上一个小节中的默认绑定Default Binding。最后this的值指向的是全局对象global或者是undefined。如果代码运行在严格模式下的话。这就是this指针“丢失”的问题

当然还有一个广为人知的就是,当我们把现在的函数保存下来然后作为回调函数callbacks传递给另外一个函数的时候,例如setTimeoue

1
2
3
4
5
6
// A dummy implementation of setTimeout
function setTimeout(callback, delay){
//wait for 'delay' milliseconds
callback();
}
setTimeout( obj.display, 1000 );

我们可以看出,当我们调用setTimeout的时候,JavaScript会把obj.display赋值给参数callback

1
callback=obj.display

熟悉的操作,跟我们上面说的一样。这种赋值操作会丢失display()的执行上下文。当这个回调函数最终被setTimeout中调用的时候,display()中的this就又变为默认绑定,指向了全局

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

Explicit Hard Binding 显示绑定

为了避免以上的情况,我们可以使用显示绑定用bind()方法把this的值绑定到函数上

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

现在呢,当我们调用outerDisplay(),在display()内部this的值就被强制绑定到obj上了

这个时候,即使我们把它当做一个回调函数来传递,也不会改变this的指向了。

用JavaScript重现React中的情形

在文章的开头,我们可以看到在Foo的React组件中,如果我们没有为事件处理函数绑定this。这个事件处理函数就被设置成了undefined

我们在开头已经说过了,在JavaScript中this绑定的机制和React无关(等一下还会说这个问题),所以我们把和React相关的代码移除,使用纯净的JavaScript例子来模拟这一问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Foo {
constructor(name){
this.name = name
}

display(){
console.log(this.name);
}
}

var foo = new Foo('Saurabh');
foo.display(); // Saurabh
// The assignment operation below simulates loss of context
// similar to passing the handler as a callback in the actual
// React Component
var display = foo.display;
display(); // TypeError: this is undefined

好,虽然我们没有把事件处理函数写出来,但是这个意思也很清楚了。在对象实例化以后,通过吧display()设置成回调,丢失了this的上下文,最后指向全局,打印出的是undefined

当然,你可能会问,this的指向不应该是global object或者是window吗?此时我们是在非严格模式下运行的啊!

其实并不是这样

class,class declarations和class expressions是运行在严格模式(strict mode)下的,例如constructor ,static方法和原型方法。 getter和setter也是运行在严格模式下的,具体请看这里

所以呢,为了避免上面那种错误,我们应该使用bind来绑定this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

更深层次的原因,React为什么会把this给搞丢呢

这个问题一直困惑了我很久,直到前几天看知乎的时候看到了react为什么绑定事件还要求开发者写代码来绑定this,为什么这样设计? - 人马座的回答 - 知乎

在React中触发事件并不是直接调用,React中的事件也并不是真正的DOM事件,而是合成事件

结合这张图来看(图有点糊了,可以右键打开到新标签页看)

React在处理事件的时候要经过事件注册和事件触发两个阶段,在事件注册的时候,会把事件当做回调函数来保存

听着很耳熟是吧,其实也就是这样。

1
var callbacks=this.handleOnclick;   //this丢失了

笔者对于Vue是一个纯小白,等使用了Vue以后再回来填坑比较React和Vue在事件处理上的区别

关于合成事件,React怎么处理合成事件,可以看这一篇博客React源码解读系列 – 事件机制,这里不再赘述

绑定this的其他方法

使用箭头函数

两种方法

  1. 在class构造函数中用箭头函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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")
);

2.在回调函数中用箭头函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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")
);

在第一种方法中,箭头函数被包裹在Fooclass中,或者说他的构造函数中,所以他的运行上下文就是我们要的已经实例化的组件

在第二种方法中,箭头函数被render()包裹,他同样也是被实例化的组件调用的,箭头函数的this也会指向组件的实例

总结

在React的Class组件中,我们用一个回调函数来传递时间处理函数

1
<button type="button" onClick={this.handleClick}>Click Me</button>

事件处理函数丢失了他的隐式绑定(lost its implicity bound context)。当事件触发的时候,this的值会按照默认绑定的方式变为undefined(因为class声明和原型方法在严格模式下运行)

当我们把事件处理函数的this绑定到组件实例上的时候,我们可以就可以把事件处理函数当做回调函数直接传递过去而不用担心丢失他的上下文。

箭头函数就不用担心这个问题了,因为他使用的是词法作用域(lexical),会把this自动绑定到他定义的作用域