0x01 When
在思考为什么 react
为什么绑定 this
之前,可以先思考下在 js
中什么时候需要绑定 this,什么时候不需要绑定 this?
var obj = {
name: 'tauysi',
foo: function () {
return this.name
},
}
// 不要绑定this的情况
obj.foo()
// "tauysi"
// 一定要绑定this的情况
var foo = obj.foo
foo()
// undefined
foo.bind(obj)()
// "tauysi"
从上面可以看出,对象方法赋值给其他的变量之后,this
就会丢失。react
之所以需要去绑定 this
的原因是因为当他调用绑定的事件处理函数时,函数的 this
丢失。这是因为 react
采用的是合成事件来标准化浏览器事件。事件依然会在真是的 dom
节点上触发,之后通过冒泡一直到 document
节点,然后分发 document
节点收集到的事件,这个时候 react
从事件触发的组件实例开始,遍历虚拟 dom
树,取下绑定的事件,收集起来,一起执行。
class Test extends React.Component {
fatherHandler = function father() {
/*...*/
}
childHander = function child() {
/*...*/
}
render() {
return (
<div onClick={this.fatherHandler}>
<span onClick={this.childHander}></span>
</div>
)
}
}
当事件触发之后 react
会将上面的事件处理函数放到一个数组中,[father, child]
,因此对函数做临时保存的时候,已经丢失了 this
。
0x02 Why
之前所提到过“在 React
组件中,每个方法的上下文都会指向该组件的实例,即自动绑定 this
给当前组件。在使用 ES6 classes
或者纯函数时,这种自动绑定就不复存在了,我们需要手动实现 this
的绑定。”那么为什么需要绑定呢?
class Toggle extends React.Component {
constructor(props) {
super(props)
this.state = { isToggleOn: true }
// This binding is necessary to make `this` work in the callback
this.handleClick = this.handleClick.bind(this)
}
handleClick() {
this.setState((prevState) => ({
isToggleOn: !prevState.isToggleOn,
}))
}
render() {
return (
<button onClick={this.handleClick}>
{this.state.isToggleOn ? 'ON' : 'OFF'}
</button>
)
}
}
在官方所给的实例当中,注释了绑定 this
的目的是为了在回调中使用 this
。官方所给的是一个 Toggle
类,在使用 ReactDom.render()
将其进行渲染到界面上时,会生成一个组件实例,而 this
最终会指向这个新生成实例。
在生成实例的过程中时,构造函数执行,this.handleClick = this.handleClick.bind(this);
,当执行到 this.handleClick
时候,会去当前实例上查找 handleClick
,如果没有会继续向上查找到原型方法中的 handleClick
,在执行 bind(this)
,将原型方法中的 this
指向新生成的实例。此时的 handleClick
方法已经由原型方法变为了实例方法。
由于原型方法的目的是实现方法的复用,现在将其变成实例方法之后,失去了复用的特性,使每个实例都有自己的状态,具有密闭性。
class Toggle extends React.Component {
constructor(props) {
super(props)
}
handleClick() {
console.log(this)
}
render() {
return (
<button onClick={() => this.handleClick()}>
Click
</button>
)
}
}
const root = ReactDOM.createRoot(
document.getElementById('root'),
)
root.render(<Toggle />)
这里,可以采用箭头函数防止丢失 this
。在 render()
函数中,利用箭头函数自身没有 this
的特性,this
指向组件实例且不再改变,再去调用 handleClick()
方法,根据谁调用 this
,this
指向谁的原则,可知 handleClick()
函数内部的 this
是指向当前组件实例的。
0x03 Class render
再从 class
的 render
函数进行思考。
render() {
return (
<button onClick={() => this.handleClick()}>
Click
</button>
);
}
上面的 render
方法,本质上是 React.createElement(component, props, ...children)
的语法糖,最后会被解析成如下的代码
render() {
return React.createElement(
"button",
{ onClick: this.handleClick },
"Click"
);
}
这里的 this.handleClick
被当作了一个参数传递给了 React.createElement
方法,此时在外部触发 click
事件的时候它的 this
就丢失了。
0x04 Hooks
这里只用 hooks
讨论 this
的问题,不做其他深究,之后会专门深入对 hooks
的理解。
hooks
复制了 class
构建组件所能实现的功能,生命周期函数、状态等,并且解决了 class
组件中 this
指向的问题。
// class组件
class Button extends React.Component {
constructor(props) {
super(props)
this.state = {
count: 0,
}
}
render() {
return (
<button
onClick={() =>
this.setState({ count: this.state.count + 1 })
}
>
Click me {this.state.count}
</button>
)
}
}
// function hooks
import { useState } from 'react'
function Button() {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count + 1)}>
Click me {count}
</button>
)
}
Button
从 class
变成了 function
并且拥有了自己的状态(count
),同时使用 setCount
进行更新维护自己的状态。