当前位置: 首页 > Web前端 > HTML

一道React面试题把我搞糊涂了

时间:2023-03-28 15:46:35 HTML

问题:在react项目中的JSX中,为什么onChange={this.func.bind(this)}比non-bindfunc=()=>{}写效率高吗?免责声明:由于本人水平有限,如有考虑不周,或有错误的地方,还望大家严格指出,万分感谢。这是我的第一篇文章,如果你有什么不明白的潜规则,请告诉我。小哥明天来分享,分享完了,继续完善。之前无意中看到这个问题。据说是阿里p5-p6级别的问题。我们先来看看这个问题。表面上是考察对react的理解深度。其实涉及到很多考点:bind,箭头函数,react中绑定this的各种方法,优缺点,适用场景,类继承,原型链等等,所以很全面。我们今天的题目就是根据这个题目总结一下相关的知识点。这里我重点分析题目中的第二种绑定方案。五种this绑定方案的区别方案一:React.createClass这是老版本React中声明组件的方式。那个版本没有引入class的概念,所以就这样创建了一个组件类(constructor)ES6class相对于createClass去掉了两点:一是mixin,二是this的自动绑定。前者可以用HOC代替,后者则完全没有。原因是FB认为这样可以避免和JS语法混淆,所以去掉了。使用这个方法,我们不用担心这个,它会自动绑定到组件实例上,但是这个API已经被弃用了,所以我们只需要了解一下就可以了。constApp=React.createClass({handleClick(){console.log(this)},render(){returnHello

}})选项2:在渲染函数中使用bindclassTestextendsComponent{handleClick(){console.log(this)}render(){return
}}方案三:在render函数中使用箭头函数classTestextendsComponent{handleClick(){console.log(this)}render(){returnthis.handleClick()}>
}}这两个解决方案简洁明了,可以传递参数,但也存在潜在的性能问题:会造成不必要的渲染。我们经常在代码中看到这些场景:更多演示案例,请点击||Immutable.Map()}data={this.state.data}onSelect={this.onSelect.bind(this)}/>//1pureComponent
}}场景一:使用空对象/数组作为封面-up解决方案,以避免在选项没有数据时运行错误。场景二:使用箭头函数绑定this。可能在一些不需要关心性能的场景下,这两种写法并没有太大的害处,但是如果我们考虑的是性能优化,比如我们使用PureComponent来优化我们的渲染性能。React使用shallowEqual作为第一层比较。这个时候我们可能会关注数据(数据是否发生变化影响渲染),但是我们忽略的选项,onSelect,会直接导致PureComponent失败。但是,我们找不到优化失败的原因。并假设我们的核心数据是不可变的,这实际上优化了我们与diff相关的性能。当数据为null时,我们期望此时不会重复渲染。但是,当我们的Test组件有状态更新,触发了Test的重新渲染,此时执行render,List还是会被重新渲染。原因是我们每次执行render,传递给子组件的options,onSelect都是一个新的对象/函数。这样,在做shallowEqual的时候,就会认为有更新,所以List组件才会更新。这个地方也有很多解决方案:不要直接在render函数中做底线,或者使用相同的引用数据源。对于事件监听函数,我们可以提前做绑定,使用方案4或5,或者最新的hook(useCallback,useMemo)constonSelect=useCallback(()=>{...//select相关的逻辑},[])//第二个参数是相关的依赖,只有当依赖发生变化时,onSelect才会发生变化,设置为空Array,即永远不会发生变化解决方案4:使用bindclassTestextendsComponent{constrcutor(){this.handleClick=this.handleClick.bind(this)}handleClick(){console.log(this)}render()intheconstructor{returnTest}}推荐这种方案通过React。在实例化组件的时候只绑定一次,然后传递相同的引用。没有解决二、三负面影响。但是这种写法比2和3要繁琐得多:1.如果我们不需要在构造函数中做任何事情,为了做函数绑定,我们需要手动声明构造函数;这并没有考虑到实例属性的新写法,直接在顶层赋值。感谢@Yesok2012指正。对于一些复杂的组件(绑定的方法太多),我们需要重复写这些方法名;我们不能单独处理参数传递问题(这一点尤为重要,同时也限制了它的使用场景)。方案五:使用箭头函数定义方式(类属性)该技术依赖于ClassProperties提案,目前还处于stage-2阶段。如果需要使用这个方案,我们需要安装@babel/plugin-proposal-class-propertiesclassTestextendsComponent{handleClick=()=>{console.log(this)}render(){returnTest}}这也是我们面试题中提到的第二个先总结一下这种绑定方案的优点:自动绑定没有方案2和方案3带来的渲染性能问题(只有bind一次,没有新的功能产生);可以再封装一下,使用params=>()=>{}这样的写法达到传递参数的目的。我们在babel上编译:点击class-properties(选择ES2016以上,需要手动安装这个插件babel-plugin-transform-class-properties比@babel/plugin-proposal-class-properties更直观,前者是babel6的命名方式,后者是babel7)详见前端高级面试题答案。我们可以看到,在使用plugin编译的版本中,这种方案其实是直接在构造函数中定义一个change属性,然后赋值给arrow函数,从而实现了对this的绑定,看起来很完美,很精致。但是,正是因为这种写法,意味着这个组件类实例化的所有组件实例都会分配一块内存来存放这个箭头函数。我们定义的普通方法其实是定义在原型对象上,所有实例共享的。牺牲的代价是我们需要使用bind来手动绑定并生成一个新的函数。让我们看一下bind函数的polyfill:返回的fBound被当作new构造函数调用returnfToBind.apply(thisinstanceoffBound?this:oThis,//获取调用(fBound)时传递的参数。bind返回的函数参数往往是这样传递的aArgs.concat(Array.prototype.slice.call(arguments)));};...//做某事returnfBound;};}如果是在不支持bind的浏览器上,其实编译之后,相当于在新生成的函数的函数体中声明:fToBind.apply(...)。我们以图片的形式来看一下差距:注:图中虚线框的区域代表引用函数节省的内存,实线框的区域代表内存消费。图1:为此绑定使用箭头函数。只有渲染函数定义在原型对象上,由所有实例对象共享。其他内存消耗基于每个实例。图2:在构造函数中执行此绑定。render和handler定义在prototype对象上,handler在instance上的实线框表示bind生成的函数消耗的内存大小。如果我们的handler函数体本身很小,实例不多,绑定的方法也不多。两种方案在内存占用上没有太大区别,但是一旦我们要在handler中处理复杂的逻辑,或者组件可能会产生大量的实例,或者组件有大量的方法需要调用绑定,第一个优势凸显。如果上面绑定this的方案只是在React中使用,或许我们只需要考虑以上几点,但是如果我们使用上面的方法创建一些工具类,我们关注的可能不止于此。说到类,大家可能会想到类继承。如果我们需要重写一个基类的某个方法,运行下面的命令,你会发现它和你想象的相去甚远。classBase{sayHello(){console.log('Hello')}sayHey=()=>{console.log('Hey')}}classAextendsBase{constructor(){super()this.name='Bitch'}sayHey(){console.log('Hey',this.name)}}newA().sayHello()//'Hello'newA().sayHey()//'Hey'注意:我们我希望打印出'Hello''HeyBitch',但实际打印出来的是:'Hello''Hey'原因很简单。在A的构造函数中,我们调用super执行Base的构造函数,为A实例添加属性。此时,Base构造函数执行后,A实例上已经存在sayHey属性,其值为箭头函数,打印Hey。我们重写的sayHey其实是在原型对象上定义的。所以最终执行的是Base中定义的sayHey方法,而不是同一个方法。基于此,我们也可以推理出如果要先执行Base的sayHey,再在此基础上执行增加逻辑怎么办?以下解决方案肯定行不通。sayHey(){super.sayHey()//报错console.log('getoff!')}再说一句:有大佬觉得这个方法性能不好,点它检查的是ops/s(每秒可以实例化多少个组件,越多越好),最后得出的结论是有人质疑这些方法最终会通过babel编译成浏览器可以识别的代码,那么finalrunningversionreflected差异是否能代表其真正的差异。具体的我没看。如果你需要了解更多,你可以阅读这篇文章类属性中的箭头函数可能没有我们想象的那么好据此,我们已经涵盖了这道题的大部分测试点。如果下次遇到这种问题,或者想到这种问题的时候,不妨从以下几个角度考虑面试官的角度:1.1在回答这个问题之前,写下并说明两种解法的原理。很明显,面试官要重点研究的是第二种情况的了解,他在幕后做了什么。然后说说他们常规的一些优缺点1.2回答关于效率的问题,前者每次绑定都会生成一个新的函数,但是函数体的代码量很少,最重要的是handler在引用的原型上,它是shared。但是对于后一种,他会在每个实例上生成一个函数。如果实例多,或者函数体大,或者绑定函数过多,占用的内存明显会超过第一个。面试官视角:测试bind的实现,测试react的绑定策略,优缺点,测试性能优化策略,测试箭头函数,测试原型链,测试继承。展开,真的很广。总结:每一种绑定方案既然存在,就有它存在的理由(除了第一个已经成为过去),但也会有相应的弊端。没有绝对的谁好谁坏。我们在使用的时候,可以根据实际情况来使用。情景选择。这个问题回答起来不难,但是要让面试官觉得你理解的很全面,还是挺难的。其次,对于this绑定方案,**如果你特别在意性能,牺牲一点代码大小和可读性:推荐四种。其次,如果你足够细心,也可以使用两个或三个,但必须注意新生成的函数是否会导致冗余渲染;如果你不想加班:建议5(在如何转推荐人一文中提到)。**