当前位置: 首页 > 科技观察

React性能优化怎么做?

时间:2023-03-21 13:12:19 科技观察

大家好,我是前端西瓜哥。今天就带大家学习如何做React性能优化。使用React.memo()一个组件可以获得一个新组件,并通过React.memo方法添加缓存。constComp=props=>{//}constMemorizedComp=React.memo(Comp);重新渲染时,如果props没有变化,则跳过组件的重新渲染,实现性能优化。这里关键是道具是不能换的,这也是最恶心的地方。对于字符串和数字等基本类型,比较没问题。但是对于对象类型,需要做一些缓存工作,让原来没有变化的对象或者函数仍然指向同一个内存对象。因为函数组件每次执行时,里面声明的函数都是一个全新的函数,指向与原函数不同的内存空间,相等比较结果为false。处理道具比较React.memo()最痛苦的部分是处理道具比较。让我们看一个例子:constMemorizedSon=React.memo(({onClick})=>{//...})constParent(){//...constonClick=useCallback(()=>{//一些复杂的判断和逻辑},[a,setA,b,onSava]);return(

)}为了保持函数的引用不变,使用了useCallback。函数中使用了一些变量,因为函数组件存在闭包陷阱,可能会导致指向旧状态的问题,所以需要判断这些变量是否发生了变化来决定是否使用缓存函数。这里有一个连锁反应,就是我还需要在变量中缓存对象类型,比如这里的setA和onSave函数。然后这些函数可以依赖其他函数,链条继续下去,然后你发现有些函数甚至来自其他组件,通过props注入。啊,我真是麻木了,我优雅的React一下子变丑了。怎么办,一种方法是使用ref。ref不存在闭包问题,每次组件更新后都能保持原点。constMemorizedSon=React.memo(({onClickRef})=>{constonClick=onClickRef.current;})constParent(){//...constonClick=()=>{//一些复杂的判断和逻辑};constonClickRef=useRef(onClick);onClickRef.current=onClick;return(
)}或constMemorizedSon=React.memo(({onClick})=>{//...})constParent(){//...constonClick=useCallback(()=>{const{a,b}=propsRef.current;const{setA,setSave}=stateRef.current;//一些复杂的判断和逻辑},[]);return(
)}当然,官方也注意到了这个场景,提出了useEvent的提议,希望尽快安装。functionChat(){const[text,setText]=useState('');constonClick=useEvent(()=>{sendMessage(text);});return;}useEvent里面的代码“看到”了调用时的props/state值。返回的函数具有稳定的标识,即使它引用的props/state发生了变化。没有依赖数组。使用useEvent后,指针稳定,不需要再添加依赖数组。有关提案的详细信息,请参阅以下链接:https://github.com/reactjs/rfcs/blob/useevent/text/0000-useevent.md。跳过中间组件假设我们的组件嵌套是这样的:A->B->C。其中,C需要获取A的状态。虽然B不需要使用A的任何状态,但是为了让C获取状态,它也通过props接收这个,然后传递给C。在这种情况下,当A更新状态时,B也会执行不必??要的重新渲染。对于这种情况,我们可以让中间组件B跳过渲染:对B应用React.memo,A的状态不再传递给B,A的状态通过发布订阅传递给C(比如useContext,或通过状态管理)。状态去中心化的假设也是组件以A->B->C的形式嵌套。C需要来自A的状态,B会通过props来帮助传递状态。当A的状态更新时,A、B、C都会被重新渲染。如果state只被C的一个组件使用,我们可以考虑直接把state传给C,这样state更新的时候,只会渲染C。组件提升将组件提升到父组件的支柱上。exportdefaultfunctionApp(){return(

Hello,world!

);}functionColorPicker({children}){让[color,setColor]=useState(“红色的”);return(setColor(e.target.value)}/>{children}
);}在这里,ColorPicker更新颜色状态后,因为ExpensiveTree来自外部props,所以不会改变,不会重新渲染。除非应用程序中有状态更改。当正确使用listkey进行列表渲染时,React会要求你提供给他们key,这样React才能识别更新后的位置变化,避免一些不必要的组件树销毁和重构工作。比如你的第一个元素是div,更新后位置改变了,第一个元素变成了p。如果不通过key通知新的位置,React会把div下的整棵树销毁,然后在p下建整棵树,非常耗性能。如果您提供一个位置,React将进行实际的DOM位置移动,然后进行树更新而不是销毁和重建。注意状态管理库的触发更新机制。对于使用Redux进行状态管理的朋友,我们会在功能组件中通过useSelector订阅状态变化,自动更新组件。constComp(){constcount=useSelector(state=>state.count);}useSelector有什么作用?它会订阅状态变化,当状态发生变化时,会运行回调函数获取返回值,并与上一次返回值进行相等比较。如果相等,则不更新组件;如果不是,请更新组件。然后缓存这次的返回值。上面的情况还好,我们来看一下写成对象的形式。import{shallowEqual,useSelector}from'react-redux'constComp(){const{count,username}=useSelector(state=>({count:state.count,username:state.username}),shallowEqual);}以上这种写法,因为默认是全等比较,所以每次更新state的时候比较结果都是false,每次更新component。对于复合对象,应该使用shallowEqual浅比较而不是相等比较,以避免不必要的更新。有个特例,假设state.userInfo有多个属性,比如username,age,acount,score,level等,有人会这样写:constComp(){const{username,age}=useSelector(state=>state.userInfo),shallowEqual);}看起来不错,但里面有一个陷阱:虽然我们的组件只使用到username和age,但是useSelector会比较整个userInfo对象。假设我们只更新userInfo.level,useSelector的比较结果为false,导致组件更新,即使你不使用level,这也不是我们所期望的。所以正确的写法应该是:constComp(){const{username,age}=useSelector(state=>{const{username,age}=state.userInfo;return{username,age};}),shallowEqual);在使用useSelector监听状态变化的时候,一定要注意状态的粒度。Contextiscoarse-grainedReact提供的Context的粒度是粗粒度的。当Context的值发生变化时,使用该Context的组件将被更新。一个问题是我们提供的Context值通常是一个对象,例如:constApp=()=>{return();}每当Context的值发生变化时,使用这个Context的组件都会更新,即使你只使用了这个值的其中一个属性,而且它并没有改变。因为Context是粗粒度的。所以你可以考虑在更高层的组件中获取Context,然后通过props注入到使用不同部分Context的组件中。顺便说一下,Context的值在需要的时候也要缓存起来,防止组件无意义的更新。constApp=()=>{constEditorContextVal=useMemo(()=>({visible,setVisible}),[visible,setVisible]);return();}批量更新有个经典问题:React的setState是同步的还是异步的?答案在副作用或合成事件响应函数中,它是异步的,将被分批执行。其他情况(比如setTimeout)会同步执行。同步的问题是组件会立即更新和渲染。如果同时有多个同步的setState,可能会出现性能问题。我们可以使用ReactDOM.unstable_batchedUpdates来批处理同步执行的状态更新。ReactDOM.unstable_batchedUpdates(()=>{setScore(score+1);setUserName('前端西瓜哥');})但是React18以后,如果开启并发模式,就不会出现同步问题,并且所有setStates都是异步的。对于Redux,可以考虑使用批量更新插件:redux-batched-actions。从'redux-batched-actions'导入{batchActions};dispatch(batchActions([setScoreAction(score+1),setUserName('前端西瓜哥')]));redux-batched-actions中间件确实会发送多个action进行打包组合然后dispatch,你会发现store.subscribe的回调函数被触发的次数较少。但是如果你使用react-redux这个库,这个库在大多数情况下其实是没用的。因为react-redux其实已经帮我们做了批处理,多次synchronizeddispatch执行后会通知组件重新渲染。延迟加载一些组件,如果可能的话,可以让组件不直接渲染,做一个延迟加载。比如:在{visible&&}的最后,React还有很多优化的方法。其中React.memo优化起来确实比较复杂,一不小心就会变成负优化。所以,不要过早优化。