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),那么建议使用第一种方式,否则的话建议使用最后一种方式。