提问:react项目中的JSX里,onChange={this.func.bind(this)}的写法,为何要比非bind的func = () => {}的写法效率高?javascript
声明: 因为本人水平有限,有考虑不周之处,或者出现错误的,请严格指出,小弟感激涕零。这是小弟第一篇文章,有啥潜规则不懂的,大家就告诉我。小弟明天有分享,等分享完了以后,继续完善。java
以前不经意间看到这道题,听说是阿里p5-p6级别的题目,咱们先看一下这道题目,明面上是考察对react的了解深度,实际上涉及的考点不少:bind,arrow function,react各类绑定this的方法,优缺点,适合的场景,类的继承,原型链等等,因此综合性很强。react
咱们今天的主题就是由此题目,来总结一下相关的知识点,这里我会着重分析题目中第二种绑定方案。git
这是老版本React中用来声明组件的方式,在那个版本,没有引入class这种概念,因此经过这种方式来建立一个组件类(constructor) ES6的class相比createClass,移除了两点:一个是mixin 一个是this的自动绑定。前者能够用HOC替代,后者则是完彻底全的没有,缘由是FB认为这样能够避免和JS的语法产生混淆,因此去掉了。 使用这种方法,咱们不须要担忧this,它会自动绑定到组件实例身上,可是这个API已经废弃了,因此只须要了解。es6
const App = React.createClass({
handleClick() {
console.log(this)
},
render() {
return <div onClick={this.handleClick}>你好</div>
}
})
复制代码
class Test extends Component {
handleClick() {
console.log(this)
}
render() {
return <div onClick={this.handleClick.bind(this)}></div>
}
}
复制代码
class Test extends Component {
handleClick() {
console.log(this)
}
render() {
return <div onClick={() => this.handleClick()}></div>
}
}
复制代码
这两个方案简洁明了,能够传参,可是也存在潜在的性能问题: 会引发没必要要的渲染github
咱们经常会在代码中看到这些场景: 更多演示案例请点击面试
class Test extends Component {
render() {
return <div>
<Input />
<button>添加<button>
<List options={this.state.options || Immutable.Map()} data={this.state.data} onSelect={this.onSelect.bind(this)} /> // 1 pureComponent
</div>
}
}
复制代码
场景一:使用空对象/数组来作兜底方案,避免options没有数据时运行时报错。 场景二:使用箭头函数来绑定this。数组
可能在一些不须要关心性能的场景下这两种写法没有什么太大的坏处,可是若是咱们正在考虑性能优化,譬如咱们使用了PureComponent
来去优化咱们的渲染性能 这里面React有使用shallowEqual作第一层的比较,这个时候咱们关注的多是这个data(数据是否有变化从而影响渲染),然而被咱们忽视的options,onSelect却会直接致使PureComponent失效,然而咱们找不到优化失败的缘由。浏览器
而假设咱们的核心data是Immutable
的,这样其实优化了咱们作diff相关的性能。当data为null时,此时咱们指望的是不会重复渲染,然而当咱们的Test组件有状态更新,触发了Test的从新渲染,此时render执行,List依旧会从新渲染。缘由就是咱们每次执行render,传递给子组件的options,onSelect是一个新的对象/函数。
这样在作shallowEqual时,会认为有更新,因此会更新List组件。性能优化
这个地方也有不少解决方案:
const onSelect = useCallback(() => {
... //和select相关的逻辑
}, []) // 第二个参数是相关的依赖,只有依赖变了,onSelect才会变,设置为空数组,表示永远不变
复制代码
class Test extends Component {
constrcutor() {
this.handleClick = this.handleClick.bind(this)
}
handleClick() {
console.log(this)
}
render() {
return <Button onClick={this.handleClick}>测试</Button>
}
}
复制代码
这种方案是React推荐的方式,只在实例化组件的时候作一次绑定,以后传递的都是同一引用,没有方案2、三带来的负面效应。
可是这种写法相对2,3繁琐了许多:
1. 若是咱们并不须要在构造函数里作什么的话,为了作函数绑定,咱们须要手动声明构造函数;这里没有考虑到实例属性的新写法,直接在顶层赋值。感谢@Yes好2012指正。
- 针对一些复杂的组件(要绑定的方法过多),咱们须要屡次重复的去写这些方法名;
- 没法单独处理传参问题(这一点尤为重要,也限制了它的使用场景)。
这种技术依赖于Class Properties
提案,目前还在stage-2
阶段,若是须要使用这种方案,咱们须要安装@babel/plugin-proposal-class-properties
class Test extends Component {
handleClick = () => {
console.log(this)
}
render() {
return <button onClick={this.handleClick}>测试</button>
}
}
复制代码
这也是咱们面试题中提到的第二种绑定方案 先总结一下优势:
- 自动绑定
- 没有方案2、三所带来的渲染性能问题(只绑定一次,没有生成新的函数);
- 能够再封装一下,使用
params => () => {}
这种写法来达到传参的目的。
咱们在babel上作一下编译:点击class-properties(选择ES2016或者更高,须要手动安装一下这个pluginbabel-plugin-transform-class-properties
相比于@babel/plugin-proposal-class-properties
更直观,前者是babel6命名方式,后者是babel7)
在使用plugin编译后的版本咱们能够看到,这种方案其实就是直接在构造函数中定义了一个change属性,而后赋值为箭头函数,从而实现的对this的绑定,看起来很完美,很精妙。然而,正是由于这种写法,意味着由这个组件类实例化的全部组件实例都会分配一块内存来去存储这个箭头函数。而咱们定义的普通方法,实际上是定义在原型对象上的,被全部实例共享,牺牲的代价则是须要咱们使用bind手动绑定,生成了一个新的函数。
咱们看一下bind函数的polyfill:
if (!Function.prototype.bind) {
... // do sth
var fBound = function() {
// this instanceof fBound === true时,说明返回的fBound被当作new的构造函数调用
return fToBind.apply(this instanceof fBound
? this
: oThis,
// 获取调用时(fBound)的传参.bind 返回的函数入参每每是这么传递的
aArgs.concat(Array.prototype.slice.call(arguments)));
};
... // do sth
return fBound;
};
}
复制代码
若是在不支持bind的浏览器上,其实编译后,也就至关于新生成的函数的函数体就一条语句: fToBind.apply(...)
。
咱们以图片的形式看一下差距:
注: 图中,虚线框面积表明引用函数所节省的内存,实线框的面积表明消耗的内存。 图一:使用箭头函数作this绑定。只有render函数定义在原型对象上,由全部实例对象共享。其余内存消耗都是基于每一个实例上的。 图二:在构造函数中作this绑定。render,handler都定义在原型对象上,实例上的handler实线框表明使用bind生成的函数所消耗的内存大小。
若是咱们的handler函数体自己就很小,实例数量很少,绑定的方法很少。两种方案在内存占用上的差别性不大,可是一旦咱们要在handler里处理复杂的逻辑
,或者该组件可能会产生大量的实例
,抑或是该组件有大量的须要绑定方法
,第一种的优点就突显出来了。
若是说上面这种绑定this的方案只用在React上,可能咱们只须要考虑上面几点,可是若是咱们使用上面的方法去建立一些工具类,可能注意的不止这些。
说到类,可能你们都会想到类的继承,若是咱们须要重写某个基类的方法,运行下面,你会发现,和想象中的相差甚远。
class Base {
sayHello() {
console.log('Hello')
}
sayHey = () => {
console.log('Hey')
}
}
class A extends Base {
constructor() {
super()
this.name = 'Bitch'
}
sayHey() {
console.log('Hey', this.name)
}
}
new A().sayHello() // 'Hello'
new A().sayHey() // 'Hey'
复制代码
注: 咱们但愿打印出 'Hello' 'Hey Bitch',实际打印的是:'Hello' 'Hey'
缘由很简单,在A的构造函数内,咱们调用super执行了Base的构造函数,向A实例上添加属性,这个时候执行Base构造函数后,A实例上已经有了sayHey属性,它的值是一个箭头函数,打印出·Hey· 而咱们重写的sayHey实际上是定义在原型对象上的。因此最终执行的是在Base里定义的sayHey方法,但不是同一个方法。 据此,咱们还能够推理一下假设咱们要先执行Base的sayHey,而后在此基础上执增长逻辑咱们又该怎么作?下面这种方案确定是行不通的。
sayHey() {
super.sayHey() // 报错
console.log('get off!')
}
复制代码
多说一句: 有大佬认为这种方法的性能并很差,它考察的点是ops/s(每秒能够实例化多少个组件,越多越好),最终得出的结论是
据此,咱们已经cover了这道题多数考点,若是下次碰到这种题,或者想出这类题不妨从下面的角度去考虑下
每种绑定方案既然存在就有其存在的理由(除了第一种已是过去),可是也会有相应的弊端,并无绝对的谁好谁差,咱们在使用时,能够根据实际场景作选择。 这道题目答到点不难,怎样让面试官以为你懂得全面仍是挺难的。
其次针对this绑定方案, 若是特别在乎性能,牺牲一点代码量,可读性:推荐四其次,若是本身自己够细心,二三也可使用,可是必定要注意新生成的函数是否会致使多余渲染; 若是想不加班:推荐五(如何传参文章中有说起)。