写在前面React提供的虚拟DOM和高效的Diff算法的完美结合,实现了DOM最小粒度的更新。在大多数情况下,React的DOM渲染效率可以满足我们的开发需求。但是在一些复杂的业务场景中,性能问题是不可避免的,我们需要采取一些措施来提升性能。其中,React组件渲染性能优化最重要的一点就是避免不必要的渲染。渲染有什么作用?DiffingReact将渲染函数返回的虚拟DOM树与旧树进行比较,以确定是否以及如何更新DOM。即使React使用高度优化的差异算法,该过程仍然会降低性能。例如,当DOM树很大时,遍历两棵树进行各种比较是相当耗费性能的,尤其是顶层setState的一个小变化,默认会遍历整棵树,而JQuery可以用一行代码。相对于手动操作DOM可能会有些挫败感,diff上的性能损失让React无法取胜。也就是说,React提供了一套方便的DOM更新机制,非常方便,性能也还可以。Reconciliation根据diff结果更新DOM树挂载或卸载DOM节点,同样会消耗性能。这部分不会过多阐述。感兴趣的同学可以移步文档对账。当render被调用时当组件被挂载时构建React组件并将DOM元素插入页面的过程称为挂载。会不会在调用setState方法的时候重新渲染但是setState执行的时候呢?答案不一定。当setState传入null时,不会触发render。不信的同学可以试试下面的democlassAppextendsReact.Component{state={a:1};render(){console.log("render");return(<>
{this.state.a}
{this.setState({a:1});//这里不改a的值}}>点我this.setState(null)}>setStatenull>;}}父组件重新渲染只要父组件重新渲染,即使子组件传入props了没有改变,那么子组件将被重新渲染。我们稍微修改了上面的demo,可以看到点击按钮的时候,Child组件的props没有变化,但是render方法也被触发了。constChild=()=>{console.log("childrender");returnchild
;};classAppextendsReact.Component{state={a:1};render(){console.log("render");return(<{this.state.a}
{this.setState({a:1});}}>点我this.setState(null)}>setStatenull>;}}我们可以做什么?上面介绍的React组件渲染机制其实是一个很好的实践,它避免了每次状态更新后都需要手动进行重新渲染操作。鱼和熊掌不可兼得,在带来方便的同时也存在一些问题,当子组件过多或者组件嵌套层级过深时,可能会因为反复重新构建而增加渲染-状态未发生变化的组件的渲染时间会影响用户体验,因此需要对React的渲染进行优化。上面说了不必要的renders会造成性能问题,所以我们主要的优化思路是减少不必要的renders。在React类组件中,使用shouldComponentUpdate或PureComponent来减少父组件更新触发的子组件渲染,从而达到目的。shouldComponentUpdate用于确定是否应该重新渲染组件。如果您不想重新渲染组件,只需返回false。你真的了解PureComponent吗?React中PureComponet的源码是if(this._compositeType===CompositeTypes.PureClass){shouldUpdate=!shallowEqual(prevProps,nextProps)||!shallowEqual(inst.state,nextState);}可以看下函数名,PureComponetShouldComponentUpdate是通过props和state的浅比较结果实现的。当对象包含复杂的数据结构时,它可能无法工作。对象的深层数据在没有触发渲染的情况下发生了变化。看到这里,顺便看看shallowEqual是怎么实现的。consthasOwnProperty=Object.prototype.hasOwnProperty;/***是判断两个值是否相等的方法。为什么要这样写MDN文档?本文不会过多阐述*https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/is*/functionis(x:mixed,y:mixed):boolean{如果(x===y){returnx!==0||y!==0||1/x===1/y;}else{returnx!==x&&y!==y;}}/****/functionshallowEqual(objA:mixed,objB:mixed):boolean{//先比较基本类型===null){returnfalse;}constkeysA=Object.keys(objA);constkeysB=Object.keys(objB);//如果长度不相等,直接返回falseif(keysA.length!==keysB.长度){returnfalse;}//当key相等时,进入循环比较for(leti=0;i}}ShouldComponentUpdate.displayName=`Pure(${BaseComponent.displayName})`;returnShouldComponentUpdate;}constPure=BaseComponent=>{consthoc=shouldComponentUpdate((props,nextProps)=>!shallowEqual(props,nextProps))returnhoc(BaseComponent);}当使用纯高级组件时,我们只需要装饰我们的子组件。importReactfrom'react';constChild=(props)=>{props.name}
;exportdefaultPure(Child);使用React.memoReact.memo是React16.6新的API,用于缓存组件的渲染,为了避免不必要的更新,它其实是一个高阶组件,和PureComponent很像,和PureComponent不同的是,React.memo只能用于函数组件的基本用法import{memo}from'react';functionButton(props){//组件代码}exportdefaultmemo(Button);高级用法默认情况下,它只会对道具进行浅层比较。遇到更深层次的复杂对象,就等??于无能为力。针对具体的业务场景,可能需要类似shouldComponentUpdate这样的API,通过memo的第二个参数实现。functionarePropsEqual(prevProps,nextProps){//yourcodereturnprevProps===nextProps;}exportdefaultmemo(Button,arePropsEqual);注意:与shouldComponentUpdate不同,arePropsEqual返回true时,不会触发render,如果返回false,就会触发。而shouldComponentUpdate正好相反。合理拆分组件微服务的核心思想是将应用垂直拆分,粒度更轻、粒度更小,让每个小应用都可以独立选择技术、开发和部署。我们在开发组件的过程中也可以使用类似的思路。想象一下,当整个页面只有一个组件时,无论哪里发生变化,都会触发整个页面的重新渲染,对整个页面进行diffing和reconciliation。拆分组件后,渲染的粒度更细,性能也能得到一定程度的提升。小结本文主要介绍如何减少不必要的渲染来提高React的性能。在实际开发过程中,前端性能问题可能并不常见。随着业务复杂度的增加,遇到性能问题的概率也会增加。减少渲染次数。类组件可以使用shouldComponentUpdate或PureComponent。功能组件可以利用高级组件的特性或者React.memo来合理拆分组件。在探索这些解决方案的同时,我们可以学习到很多经典的编程思想,从而更合理地使用框架和技术来解决业务问题。