All articles

React class's this binding problem

Nov 15, 2022

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() 方法,根据谁调用 thisthis 指向谁的原则,可知 handleClick() 函数内部的 this 是指向当前组件实例的。

0x03 Class render

再从 classrender 函数进行思考。

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>
  )
}

Buttonclass 变成了 function 并且拥有了自己的状态(count),同时使用 setCount 进行更新维护自己的状态。

Reference

antcao.me © 2022-PRESENT

: 0x9aB9C...7ee7d