带你找出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>; } }
可以看出,3
和2
从最大的区别在于,3
将fn
直接绑定在实例的属性上(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
绑定事件的几种区别时,相信大家都能答出来了。。。。