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
// App.jsimportEditorfrom'./Editor'importStatusBarfrom'./StatusBar'exportdefaultclassApp{constructor(props){super(props){this.editorRef=React.createRef()}}render(){return(// ... other components<EditorProvider><div><Explorer/><EditoreditorRef={this.editorRef}/></div><StatusBar/></EditorProvider>)}}
最近项目一直在使用 Monaco Editor 这个库. 在我加了一个新功能之后, 整个编辑器开始变的非常卡, 我试图解决这个性能问题. 但是发现有一些棘手...
评论以及文末有更新
场景
其实我加的新功能很简单, 以 vscode 为例, 他底部有一个 status bar, 用于显示当前编辑器的一些信息. 我当时加的功能是模仿 vscode, 在底部增加一个能显示当前光标位置/选中位置, 以及选中的单词的长度. 由于是公司代码的原因我这里只是简单放一下伪代码, 实际代码会比这个这个伪代码复杂和抽象很多
文件结构:
简单说明一下, 有一个组件叫
App
, 由于历史遗留原因这二组件是个class
组件, 而且代码很多, 这个组件下有非常多的子组件,Editor
和StatusBar
就是其中的两个子组件而我现在的需求是: 用户在
Editor
里做任何操作,StatusBar
组件都需要显示出对应用户光标的位置我思路也很简单:
cursor
(光标) 部分的状态在Editor
中是可以拿到的StatusBar
这个组件需要根据cursor
状态来渲染, 因此做状态提升, 到App
中App
中初始化cursor
状态, 传入相关setState()
回调函数到Editor
中,Editor
组件调用更新cursor
状态, 最后StatusBar
获取cursor
最新的状态进行渲染.实现
所以代码可能长这样
看上去好像没啥问题, 我当时这么写也没考虑太多
问题
实际上最后写完我测试发现了很大的性能问题
其实很简单, 我每次在
Editor
里面调用父组件(App
) 的setState()
, 都会导致父组件重新渲染. 而App
这个组件是一个很大的类组件, 里面还渲染很多别的组件, 而用户在编辑器里面只要敲一点东西, 光标几乎都会改变(或者直接点, 用户闲着没事在编辑器里直接乱点一通, 也能达到相同的效果). 这直接导致重新渲染App
的频率相当频繁...然后我页面就卡爆了...
我当时有点懵, 因为说实话这种渲染确实好像没法避免, 我每次的
cursor
状态确实不一样, 我没法直接通过shouldComponentUpdate
来避免不必要的重复渲染当然有时候可以避免, 就是当用户的每次都点击同一个地方,
cursorPosition
就一样了...方法
我一开始想到的一个办法是, 将
Editor
组件和StatusBar
重新写在一个新的组件, 这样所有的状态就只在这个新组件(可以看成这是一个中间组件)里面管理, 不会触发App
这个大组件的重新渲染了.但我还是很快放弃了这个想法, 因为把
StatusBar
和Editor
放一起其实不简单.... 从 dom 上来看, 他们其实不是严格的兄弟组件, 中间还有着别的组件, 如果要抽出来还必须连带着别的组件一起重写:总之, 我这么重构 effort 其实挺大...
可能的解决方案?
后来和另一个组里的实习生交流的时候, 他提出尝试把
cursor
部分的状态放到redux
里面管理, 在Editor
里面dispatch
相应改变cursorPosition
的action
, 在StatusBar
里面连接 redux 拿到最新的cursorPosition
状态. 这样直接绕过App
这一层, 避免了重复渲染其实这个思路和之前的想法类似 都是要避开
App
这个大组件, 只不过用 redux 这种状态管理库似乎代码写起来简单一些代码最后就变成这样了:
不过这么讲也存在别的一些问题:
cursorPosition
这个状态只在一个组件里被用到, Redux 本意还是为了做状态的管理, 多个组件可能都会共享到这个状态, 但现在只有一个, 似乎有点大材小用了可能有更好的解法?
如果你恰好能明白我在讲什么, 并且有更好的办法, 请务必告诉我...
更新
文章发布之后有几位前辈评论了一下, 都非常好. 我自己总结然后实践了一下, 以下是我的解决思路
不管是我之前用的 Redux, 还是评论里面提到的 Context, 其实都是 Pub/Sub (发布订阅)这个思想的实践. 不过由于目前使用 Redux 有点太重, 所以其实用 Context 会更好
不过在使用 Context 的时候存在一个问题: 就是如果
context
的value
是一个对象这种复杂结构, 然后存在多个消费者, 每个消费者可能只是订阅一部分value
. 但是由于context
的设计, 只要value
部分变了, 那么所有的消费者都会被通知, 那么有很大的可能所有的消费者组件都被重新渲染了.基于这个问题 Dan Abramov 也是有给出一些解法. 其实最直白的做法就是将多个 context 分离成几个更小的. 不过我在搜索的过程中发现了另外一个似乎更精巧的解法, 虽然可能有点简陋. 但是用在我们项目里我觉得应该够了(其实我不确定, 等过两天 mentor 给我 review 代码的时候再问问)?
context
先看看最基本的使用 context 的做法, 也是评论里 @李引证 提到的做法, 不过这里我略有修改.
这里我选择用
useReducer()
也是因为其实我的editor
状态有点复杂, 而且cursorPosition
和selections
是对应两个不同的消费者组件. 如果单纯这么用, 其实是有一点我之前提到的性能问题的mapStateToProps
参考了 React Context API and avoiding re-renders 这个问题下的一个回答. 其实核心就在于用
React.memo
以及将相关对应的context
上的value
map 到对应的消费者组件的props
上, 相关props
变了, 组件才重新渲染. 虽然感觉兜兜转转绕了半天又绕到了 Redux 上....结合我之前的
Editor
和StatusBar
一起用:如果你有更优雅的解法, 请一定告诉我...
The text was updated successfully, but these errors were encountered: