带你找出react中,回调函数绑定this最完美的写法!

  • 2020 年 3 月 10 日
  • 筆記

相信每一个人写过react的人都对react组件的的this绑定有或多或少的了解

在我看来,有若干种this写法,我们通过本文,一步步找优缺点,筛选出最完美的react this写法!(有点小激动)

1、远古时代 React.createClass

说实话,在我接触react的时候,这种写法就只在相关文章见到了。React.createClass会自动绑定所有函数的this到组件上

React.createClass({      fn() {          //  this 指向组件本身          console.log(this);      },      render() {          return <div onClick={this.fn}></div>;      }  });

react 0.13开始就已经支持class声明组件了。react 16已经废弃了这种写法,这里就不讨论了。直接淘汰

2、错误示范

class App extends React.Component {      fn() {          console.log(this);      }      render() {          return <div onClick={this.fn}></div>;      }  }

这种写法,最终打印this是指向undefined。原因在于上面的事件绑定函数调用可以看作如下。

// 伪代码  onClick = app.fn;  onClick();

onClick进行调用时,this的上下文是全局,由于是在es module中,全局this指向undefined,所以这个错误示范的事件处理函数中的this不是指向组件本身的

3、利用proposal-class-public-fields直接绑定箭头函数

class App extends React.Component {      fn = () => {          console.log(this);      };      render() {          return <div onClick={this.fn}></div>;      }  }

目前proposal-class-public-fields仍处于提案阶段,需要借助@babel/plugin-proposal-class-properties这个 babel 插件在浏览器中才能正常工作

经过babel转换,等价于以下的代码

class App extends React.Component {      constructor(props) {          super(props);          this.fn = () => {              console.log(this);          };      }      render() {          return <div onClick={this.fn}></div>;      }  }

可以看出,32从最大的区别在于,3fn直接绑定在实例的属性上(2是绑定在原型的方法上),并利用箭头函数继承父级this作用域达到了this绑定的效果。

优点:代码十分简洁,不需要手动写bind、也不需要在constructor中进行额外的操作

缺点:很多文章都提到这是一种完美写法,但其实每一个实例在初始化的时候都会新建一个新事件回调函数(因为绑定在实例的属性上,每个实例都有一个fn的方法。本质上,这是一种重复浪费),所以其实并不是很完美

4、Constructor中使用 bind

class App extends React.Component {      constructor(props) {          super(props);          this.fn = this.fn.bind(this);      }      fn() {          console.log(this);      }      render() {          return <div onClick={this.fn}></div>;      }  }

优点:fn函数在组件多次实例化过程中只生成一次(因为是用实例的fn属性直接指向了组件的原型,并绑定了this属性)

缺点:代码写起来比较繁琐,需要在constructor中,手动绑定每一个回调函数

5、在render中进行bind绑定

class App extends React.Component {      fn() {          console.log(this);      }      render() {          return <div onClick={this.fn.bind(this)}></div>;      }  }

优点:fn函数多次实例化只生成一次,存在类的属性上,类似于4,写法上比4稍微好一点。

缺点:this.fn.bind(this)会导致每次渲染都是一个全新的函数,在使用了组件依赖属性进行比较、pureComponent、函数组件React.memo的时候会失效。

最关键的是5的写法会被6全方面吊打完爆

6、箭头函数内联写法

class App extends React.Component {      fn() {          console.log(this);      }      render() {          return <div onClick={() => fn()}></div>;      }  }

优点:

1、写法简洁

2、与2-5的写法相比,6写法最大的最大好处就是传参灵活

3、全面吊打写法4,相同的缺点,但是多了传参数灵活。如果需要渲染一个数组,并且数组根据不同项,事件处理不一样时,2-5就很尴尬了

const arr = [1, 2, 3, 4, 5];  class App extends React.Component {      fn(val) {          console.log(val);      }      render() {          return (              <div>                  {arr.map(item => (                      // 采用 6的写法,要打印数组这一项就很方便                      <button onClick={() => this.fn(item)}>{item}</button>                  ))}              </div>          );      }  }

网上看多文章都在使用3的方案的时候推荐使用闭包传参实现该效果

const arr = ["1", "2", "3", "4", "5"];  class App extends React.Component {      fn = val => () => {          console.log(val);      };      render() {          return (              <div>                  {arr.map(item => (                      // 每次也生成了全新的函数了                      <button onClick={this.fn(item)}>{item}</button>                  ))}              </div>          );      }  }

经过前面的分析。使用这种写法,还不如直接使用6的内联写法,两种每次都是返回全新的函数,而且,少了一次返回闭包函数的开销。

缺点: 每次渲染都是一个全新的函数,类似于5的缺点,在使用了组件依赖属性进行比较、pureComponent、函数组件React.memo的时候会失效

7、函数组件的useCallback

虽然函数组件无this一说法,但既然讲到react回调函数,还是提一下

hook出现之前,函数组件是不能保证每次的回调函数都是同一个的,(虽然可以把回调提到函数作用域外固定,但都是一些 hack 的方法了)

const App = () => {      // 每次都是全新的      return <div onClick={() => console.log(2333)}></div>;  };

有了hook。我们便可以使用useCallback固定住回调

const App = () => {      const fn = useCallback(() => console.log(2333), []);      // 每次都是固定      return <div onClick={fn}></div>;  };

有没有发现。其实很类似class组件的将回调挂在class上,嗯,这就hook强大的地方,利用了react fiber,挂在了它的memorizeState上,实现了能在多次渲染中保持(这就不展开讲了)。缺点还是和上面提过的,参数传递不方便,如渲染数组

8、(最完美)的写法?

当然,如果不使用内联写法又获取到参数行不行呢。当然也是可以的,利用元素的自定义属性data-属性传递参数

const arr = ["1", "2", "3", "4", "5"];  class App extends React.Component {      constructor(props) {          super(props);          this.fn = this.fn.bind(this);      }      fn(e) {          // 1 2 3 4 5          console.log(e.target.dataset.val);      }      render() {          return (              <div>                  {arr.map(item => (                      // 每次也生成了全新的函数了                      <button data-value={item} onClick={this.fn}>                          {item}                      </button>                  ))}              </div>          );      }  }

orz! 这是最完美写法了吧!不考虑代码繁琐的情况下,既正确绑定了this,又不会多次实例化函数,又能渲染数组。。

其实还是错误的…data-xxx属性只能传递string类型的数据,因为是附加给html的,react会进行一步JSON.stringify的操作,如果你传递一个对象,打印出来是value: "[object Object]"。果然,就算是为了获取字符串参数,也不推荐这种写法。可以,但没必要!

9、最省事的写法?

有一位大佬写了一个 babel 插件babel-plugin-react-scope-binding的插件,能够实现 将2的错误示范自动转化内联函数,更牛逼的是还能传参。介绍。确实是最省事的写法,不过很容易引起歧义,也有上面提到的问题

好吧,感谢你看到这里,废话连篇一篇文章,其实似乎并没有找回完美的写法。。。

下面说说本人的一些愚见吧

在平时写代码中,在render没有非常大的开销情况下(也没有依赖组件的某些属性进行性能优化、没使用 pureComponent), 会优先使用纯内联的写法(无论是函数组件还是 class 组件)。因为重新创建函数开销我觉得不是特别大的,并且内联我觉得还有最大的好处就是,看到一个事件调用,不需要再点到事件函数调用的地方…减少了飞来飞去的情况,而且上面也提到,内联传递参数是非常方便的。在实在遇到性能问题,再考虑优化。无需为了优化而优化

最近春招季,看完这篇文章,虽然还是找不出最完美的react绑定事件写法,但是面试官提起react绑定事件的几种区别时,相信大家都能答出来了。。。。