React基础12 React中的this绑定
React组件中的this绑定到底是怎么一回事?学习一下。
为什么要bind(this)
React的Class组件中常常会用到bind(this)
来绑定this
,但是究竟为什么要这么做呢?难道Class中的方法拿不到实例的this
吗?
来试验一下:
1 | class Person { |
上面代码的Person
类中,say
方法去获取了this
,然后当我们实例化一个对象时,并调用say()
方法时,结果是:
1 | let p1 = new Person('Jay'); |
结果说明在Class的方法中,是可以直接通过this
获得实例对象的。所以实际上在React的Class中直接使用this
是没有问题的,例如在生命周期函数或者render
中。但是在render
函数中的JSX模板中的事件处理函数,这里面的调用的方法的this
就不会指向组件实例:
1 | export default class Demo5 extends Component { |
上面的handleClick
函数作为JSX中的事件处理函数,其中的this
不会指向组件实例,而是指向了事件响应的上下文环境(非严格环境下是window
,严格环境下是undefined
),而类声明和类表达式的主体以严格模式执行(主要包括构造函数、静态方法和原型方法以及getter
和setter
函数),所以this
指向undefined
为什么this
不能指向组件实例
实际上这与React和JSX语法没有关系,是JavaScript的this
绑定机制导致了上述情况的发生。要明确的是,函数内部的this
取决于该函数被调用时的上下文环境。
默认绑定
1 | function display(){ |
在上面的情况下,display
方法中的this
在非严格模式下指向window
,在严格模式下指向undefined
隐式绑定
1 | const obj = { |
在上面的情况下,通过obj
对象来调用这个函数时,display
内部的this
指向了obj
。
但是如果将这个函数赋值给其他变量,并且通过这个变量去调用该函数时,在display
中获得this
就不同了:
1 | var name = 'hello'; |
我们调用outer
时,并没有指定一个具体的上下文对象,这个时候this
值与默认绑定的结果是相同的,在非严格模式下指向window
,在严格模式下指向undefined
在将一个方法以回调的形式传递给另外一个函数,或者像setTimeout
这样的内置JavaScript函数时,就可以依照上面的过程进行判断
例如我们自定义一个setTimeout
方法并调用,预测一下会发生什么:
1 | //setTimeout 的虚拟实现 |
在调用setTimeout
时,函数内部将obj.display
赋值给参数callback
:
1 | callback = obj.display; |
在一段时间后调用这个方法时,调用的实际上是callback()
,而这种调用会让display
方法丢失上下文,其中的this
会退回至默认绑定,指向全局变量
1 | var name = "uh oh! global"; |
显示绑定
为了避免上面的情况,可以使用bind
来显式的为方法绑定上下文:
1 | var name = "uh oh! global"; |
绑定了this
后,在调用对应的方法也能够渠道我们绑定的上下文。同理,粗若将obj.display
作为callback
参数传递给函数,display
中的this
也会正确指向obj
React编译时的处理
明确了隐式绑定时,将方法作为参数传递给另一个函数时会导致该方法的上下文丢失。而在React的类的写法中,JSX的事件处理程序的this
会丢失,就是因为这个原因。
在编译JSX的过程中,事件处理程序会作为属性值被放置在一个对象中,调用时会识别为函数调用模式,上下文丢失。
举个例子,下面的Class组件:
1 | export default class Demo extends Component { |
编译完是下面这样的:
1 | const Demo = function (_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 | export default class Demo extends Component { |
上面的写法会会保证handleClick
内的this
被正确绑定,要注意的是这是一个实验性的语法,但是Create React App默认启用了这个语法。
如果没有使用Create React App的话需要手动开启这个语法,使用Babel的transform-class-properties
或者enable stage-2 in Babel
这两项功能
(2)第种方式是在JSX的回调函数中使用箭头函数:
1 | export default class Demo extends Component { |
这种语法的主要问题还是来自于性能的担忧,因为每次渲染组件时都会创建不同的回调函数。大多数情况时没有什么问题,但是如果该回调函数作为prop传入子组件时,这些组件可能会进行额外的重新渲染。
(3)第三种方式是在JSX的回调函数中使用bind
进行绑定:
1 | export default class Demo extends Component { |
这种方法的问题和上一种是类似的,有可能带来性能的问题。
(4)第四种方式是在构造函数constructor
中进行显示的绑定:
1 | export default class Demo extends Component { |
这样就可以避免第二种方法可能带来的性能问题,而且很直观,但问题是增加了代码数量,而且为了绑定this
必须声明constructor
,在constructor
中还不能忘记super
,有点麻烦
所以,从性能角度上考虑,如果开启了对应的public class fileds
语法(使用了Create React App),那么建议使用第一种方式,否则的话建议使用最后一种方式。