在大厂写React,我学到了什么?性能优化
时间:2023-03-15 13:42:30
科技观察
前言我工作中的技术栈主要是React+TypeScript。在这篇文章中,我想总结一下如何在项目中使用React的一些技巧来进行性能优化或更好的代码组织。性能优化的重要性不用多说,谷歌发布的很多调查都准确地展示了性能对网站留存率的影响,而代码组织优化关系到后续的维护成本,同事在维护你的代码时“甜言蜜语”的频率??,看完本文,你一定会有所收获。神奇宝贝们,我们有一个需要通过Provider传递一些主题信息给子组件:看这段代码:log("渲染了不关心皮肤的子组件");return
我不关心皮肤,不要让我在皮肤变化时重新渲染!
;}exportfunctionChildWithTheme(){consttheme=useContext(ThemeContext);return
我有皮肤~{theme}
;}exportdefaultfunctionApp(){const[theme,setTheme]=useState("light");constonChangeTheme=()=>setTheme(theme==="light"?"dark":"light");return(
改变皮肤);}这段代码看起来没有问题,并且很符合撸起袖子干的直觉,但是会让ChildNonTheme这个不关心皮肤的子组件在皮肤状态改变的时候无效的重新渲染。这本质上是因为React是从上到下递归更新的。这样的代码将被babel翻译成像React.createElement(ChildNonTheme)这样的函数调用。React官方经常强调props是不可变的,所以每次调用一个功能组件时,都会生成一个新的props引用。查看createElement的返回结构:constchildNonThemeElement={type:'ChildNonTheme',props:{}//<-thisreferencehasbeenupdated}就是因为这个新的props引用,ChildNonTheme组件也重新渲染了。那么如何避免这种无效的重新渲染呢?关键词是“明智地使用孩子”。importReact,{useContext,useState}from"react";constThemeContext=React.createContext();functionChildNonTheme(){console.log("渲染不关心皮肤的子组件");return我不关心皮肤,皮肤改变时不要让我重新渲染!
;}functionChildWithTheme(){consttheme=useContext(ThemeContext);return我有皮肤~{theme}
;}functionThemeApp({children}){const[theme,setTheme]=useState("light");constonChangeTheme=()=>setTheme(theme==="light"?"dark":"light");return(更换皮肤{children});}exportdefaultfunctionApp(){return();}没错,唯一不同的是我把控制状态的组件和负责显示的子组件分开了,传入children后直接渲染,因为孩子是从外面进来的,也就是说,ThemeApp组件内部不会有React.createElement这样的代码,所以setTheme触发重新渲染后,children根本没有变化,可以直接复用。我们看一下ThemeApp包裹的那个,它会作为child传递给ThemeApp,ThemeApp内部的更新根本不会触发外部的React.createElement,所以会直接复用之前的元素。结果://完全重用,道具不会改变。constchildNonThemeElement={type:ChildNonTheme,props:{}}换皮后,控制台是空的!实现了优化。综上所述,渲染起来比较耗时,但是应该将不需要关心状态的子组件提升到“有状态组件”的外部,作为子组件或者props传入,直接使用,以防止它们被一起渲染。神奇宝贝-在线调试地址当然这个优化也可以通过用React.memo包裹子组件来完成,但是会相对增加维护成本,所以根据场景选择。上下文读写分离想象一下,现在我们有一个全局的日志记录需求,我们想通过Provider来做,很快代码就会写成:importReact,{useContext,useState}from"react";import./styles.css";constLogContext=React.createContext();functionLogProvider({children}){const[logs,setLogs]=useState([]);constaddLog=(log)=>setLogs((prevLogs)=>[...prevLogs,log]);return({children});}functionLogger1(){const{addLog}=useContext(LogContext);console.log('Logger1render')return(<一个可以发送日志的组件1
addLog("logger1")}>发送日志/>);}functionLogger2(){const{addLog}=useContext(LogContext);console.log('Logger2render')return(<>一个可以发送日志的组件2
addLog("logger2")}>发送日志/>);}functionLogsPanel(){const{logs}=useContext(LogContext);returnlogs.map((log,index)=>{日志;);}exportdefaultfunctionApp(){return({/*写入日志*/>{/*读取日志*/> );}我们已经使用了上一章的优化技巧,将LogProvider单独封装,将子组件提升到外层传入,先想想最好的情况。Logger组件只负责发送日志。如果不关心日志的变化,当任何组件调用addLog写入日志时,理想情况下应该只重新渲染LogsPanel组件。但是,这样的代码编写会导致每次任何组件写入日志时,所有的Loggers和LogsPanels都要重新渲染。这绝对不是我们所期望的。假设在真实场景的代码中,有很多组件可以写日志。每次写完,全局组件都会重新渲染?这当然是不能接受的。出现这个问题的本质原因在官网的Context部分说的很清楚:当子组件调用LogProvider中的addLog时,导致LogProvider重新渲染,传递给Provider的值必然会发生变化。由于该值包含了logs和setLogs属性,所以这两个A改变其中任何一个都会导致所有订阅LogProvider的子组件重新渲染。那么解决方法是什么?其实就是读写分离。我们通过不同的Provider传递logs(读)和setLogs(写),让负责写的组件改变日志,其他“写组件”不会重新渲染,只有真正关心日志的“读组件”才会被重新渲染。functionLogProvider({children}){const[logs,setLogs]=useState([]);constaddLog=useCallback((log)=>{setLogs((prevLogs)=>[...prevLogs,log]);},[]);return(
{children});}刚才我们说了要保证值引用是不能改变的,所以这里自然要用useCallback把addLog方法包裹起来,保证LogProvider重新渲染的时候传递给LogDispatcherContext的值不会改变。现在,如果我从任何“写入组件”发送日志,它只会呈现“读取组件”LogsPanel。Context读写分离-在线调试Context代码组织上面的案例,我们在子组件中获取全局状态,都是直接使用useContext:importReactfrom'react'import{LogStateContext}from'./context'functionApp(){constlogs=React.useContext(LogStateContext)}但是有没有更好的方式来组织代码呢?例如:importReactfrom'react'import{useLogState}from'./context'functionApp(){constlogs=useLogState()}//contextimportReactfrom'react'constLogStateContext=React.createContext();exportfunctionuseLogState(){returnReact.useContext(LogStateContext)}添加一些稳健性保证?importReactfrom'react'constLogStateContext=React.createContext();constLogDispatcherContext=React.createContext();exportfunctionuseLogState(){constcontext=React.useContext(LogStateContext)if(context===undefined){thrownewError('useLogStatemustbeusedwithinaLogStateProvider')}returncontext}exportfunctionuseLogDispatcher(){constcontext=React.useContext(LogDispatcherContext)if(context===undefined){thrownewError('useLogDispatchermustbeusedwithinaLogDispatcherContext')}returncontext}如果某些组件需要同时读写日志,调用两次很麻烦?exportfunctionuseLogs(){return[useLogState(),useLogDispatcher()]}exportfunctionApp(){const[logs,addLogs]=useLogs()//...}根据场景,灵活运用这些技巧,让你的代码更加健壮优雅~CombiningProviders假设我们使用上面的方法来管理一些小的全局状态。Provider越来越多,有时候我们会遇到嵌套地狱:constStateProviders=({children})=>({children}有什么办法吗>)functionApp(){return()}解决方案?当然我们参考redux中的compose方法,自己写一个composeProvider方法:functioncomposeProviders(...providers){return({children})=>providers.reduce((prev,Provider)=>{prev},children,)}代码可以简化如下:本文主要围绕ContextAPI,讲了几个性能优化和代码组织优化点。总结就是:尝试改进不相关的子组件元素的渲染到“有状态组件”之外,必要时将Context的读写分开。包裹Context的使用,注意错误处理。结合多个上下文来优化代码。本文转载自微信公众号《前端从进阶到入学》,可关注下方二维码。转载本文请联系前端从进阶到录取公众号。