You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
functionLogProvider({ children }){const[logs,setLogs]=useState([]);constaddLog=useCallback(log=>{setLogs(prevLogs=>[...prevLogs,log]);},[]);return(<LogDispatcherContext.Providervalue={addLog}><LogStateContext.Providervalue={logs}>{children}</LogStateContext.Provider></LogDispatcherContext.Provider>);}
我们刚刚也提到,需要保证 value 的引用不能发生变化,所以这里自然要用 useCallback 把 addLog 方法包裹起来,才能保证 LogProvider 重渲染的时候,传递给的LogDispatcherContext的 value 不发生变化。
importReactfrom'react';constLogStateContext=React.createContext();constLogDispatcherContext=React.createContext();exportfunctionuseLogState(){constcontext=React.useContext(LogStateContext);if(context===undefined){thrownewError('useLogState must be used within a LogStateProvider');}returncontext;}exportfunctionuseLogDispatcher(){constcontext=React.useContext(LogDispatcherContext);if(context===undefined){thrownewError('useLogDispatcher must be used within a LogDispatcherContext');}returncontext;}
constStateProviders=({ children })=>(<LogProvider><UserProvider><MenuProvider><AppProvider>{children}</AppProvider></MenuProvider></UserProvider></LogProvider>);functionApp(){return(<StateProviders><Main/></StateProviders>);}
前言
我工作中的技术栈主要是 React + TypeScript,这篇文章我想总结一下如何在项目中运用 React 的一些技巧去进行性能优化,或者更好的代码组织。
性能优化的重要性不用多说,谷歌发布的很多调研精确的展示了性能对于网站留存率的影响,而代码组织优化则关系到后续的维护成本,以及你同事维护你代码时候“口吐芬芳”的频率 😁,本篇文章看完,你一定会有所收获。
神奇的 children
我们有一个需求,需要通过 Provider 传递一些主题信息给子组件:
看这样一段代码:
这段代码看起来没啥问题,也很符合撸起袖子就干的直觉,但是却会让
ChildNonTheme
这个不关心皮肤的子组件,在皮肤状态更改的时候也进行无效的重新渲染。这本质上是由于 React 是自上而下递归更新,
<ChildNonTheme />
这样的代码会被 babel 翻译成React.createElement(ChildNonTheme)
这样的函数调用,React 官方经常强调 props 是 immutable 的,所以在每次调用函数式组件的时候,都会生成一份新的 props 引用。来看下
createElement
的返回结构:正是由于这个新的 props 引用,导致
ChildNonTheme
这个组件也重新渲染了。那么如何避免这个无效的重新渲染呢?关键词是「巧妙利用 children」。
没错,唯一的区别就是我把控制状态的组件和负责展示的子组件给抽离开了,通过 children 传入后直接渲染,由于 children 从外部传入的,也就是说
ThemeApp
这个组件内部不会再有React.createElement
这样的代码,那么在setTheme
触发重新渲染后,children
完全没有改变,所以可以直接复用。让我们再看一下被
ThemeApp
包裹下的<ChildNonTheme />
,它会作为children
传递给ThemeApp
,ThemeApp
内部的更新完全不会触发外部的React.createElement
,所以会直接复用之前的element
结果:在改变皮肤之后,控制台空空如也!优化达成。
总结下来,就是要把渲染比较费时,但是不需要关心状态的子组件提升到「有状态组件」的外部,作为 children 或者 props 传递进去直接使用,防止被带着一起渲染。
神奇的 children - 在线调试地址
当然,这个优化也一样可以用 React.memo 包裹子组件来做,不过相对的增加维护成本,根据场景权衡选择吧。
Context 读写分离
想象一下,现在我们有一个全局日志记录的需求,我们想通过 Provider 去做,很快代码就写好了:
我们已经用上了上一章节的优化小技巧,单独的把
LogProvider
封装起来,并且把子组件提升到外层传入。先思考一下最佳的情况,
Logger
组件只负责发出日志,它是不关心logs
的变化的,在任何组件调用addLog
去写入日志的时候,理想的情况下应该只有LogsPanel
这个组件发生重新渲染。但是这样的代码写法却会导致每次任意一个组件写入日志以后,所有的
Logger
和LogsPanel
都发生重新渲染。这肯定不是我们预期的,假设在现实场景的代码中,能写日志的组件可多着呢,每次一写入就导致全局的组件都重新渲染?这当然是不能接受的,发生这个问题的本质原因官网 Context 的部分已经讲得很清楚了:
当
LogProvider
中的addLog
被子组件调用,导致LogProvider
重渲染之后,必然会导致传递给 Provider 的 value 发生改变,由于 value 包含了logs
和setLogs
属性,所以两者中任意一个发生变化,都会导致所有的订阅了LogProvider
的子组件重新渲染。那么解决办法是什么呢?其实就是读写分离,我们把
logs
(读)和setLogs
(写)分别通过不同的 Provider 传递,这样负责写入的组件更改了logs
,其他的「写组件」并不会重新渲染,只有真正关心logs
的「读组件」会重新渲染。我们刚刚也提到,需要保证 value 的引用不能发生变化,所以这里自然要用
useCallback
把addLog
方法包裹起来,才能保证LogProvider
重渲染的时候,传递给的LogDispatcherContext
的 value 不发生变化。现在我从任意「写组件」发送日志,都只会让「读组件」
LogsPanel
渲染。Context 读写分离 - 在线调试
Context 代码组织
上面的案例中,我们在子组件中获取全局状态,都是直接裸用
useContext
:但是是否有更好的代码组织方法呢?比如这样:
在加上点健壮性保证?
如果有的组件同时需要读写日志,调用两次很麻烦?
根据场景,灵活运用这些技巧,让你的代码更加健壮优雅~
组合 Providers
假设我们使用上面的办法管理一些全局的小状态,Provider 变的越来越多了,有时候会遇到嵌套地狱的情况:
有没有办法解决呢?当然有,我们参考
redux
中的compose
方法,自己写一个composeProvider
方法:代码就可以简化成这样:
总结
本篇文章主要围绕这 Context 这个 API,讲了几个性能优化和代码组织的优化点,总结下来就是:
欢迎关注「前端从进阶到入院」,还有很多前端原创文章哦~
The text was updated successfully, but these errors were encountered: