
问题背景
这周在帮助b哥完成他的项目前端的时候遇到了一个有趣的问题。
先介绍下背景,这是一个企业内即时通讯工具,主要的页面布局,从上往下依次包括消息列表、选择面板、操作栏、输入框。
初次尝试:DOM结构调整
在一开始的时候,b哥提出的问题在于如何修改CSS让选择面板正确的渲染到操作栏的上方而不是下面。
这个我想到的当然是直接修改dom结构,何必要用CSS来实现呢,很快就实现了这个需求。
隐藏的挑战:异步渲染顺序
然而在之后却发现了新的问题,因为选择面板和消息列表的加载其实是依赖于API的。在当时我们反复测试的情况是,操作栏先渲染成功,然后是消息列表,最后是选择面板。
对于除了消息列表之外的其他组件,其实是会占据空间然后导致消息列表的可见范围越来越短的。
在操作栏渲染之后,消息列表才开始渲染,一切都显得很正常。而当选择面板出现的时候,消息列表的高度变小了,但是内容却没有维持在最新的位置。
尝试CSS解决方案
这个问题看起来很简单的。b哥要求使用css来完成这个任务,询问AI之后,给出的是flex的column-reverse方案,这个方法看起来可行,实际上却并没有解决问题。
回顾现有代码:JavaScript解决方案
当重新阅读代码的时候,发现其实之前的代码使用了一个Javascript方法,在数据加载完成后,手动调用面板的ScrollIntoView方法,以适应高度的变化。
那么使用纯CSS就没法实现这个功能了,因为这个方法已经在多个地方使用,我们只能继续在合适的地方继续使用而不是自己新建一套CSS方案。
脱敏示例代码如下
function recalcposition() {
setTimemout(() => {
panelRef.current.scrollIntoView({ behavior: "instant", block: "end" });
}, 50)
}
探索DOM观察者API
一开始我想的是能否找到一个onResize的事件来监听页面的变化,但是实际上React似乎并没有封装这么一套系统,而我又不想使用原生的接口,于是乎想到了IntersectionObserver类似的DOM接口。
AI推荐了ResizeObserver/MutationObserver。当使用ResizeObserver时,我们发现成功了,但是却发生了新的问题:
新问题:渲染延迟
每一次都是选择面板渲染成功之后,经过了一个定量的延迟,消息面板才会滚动到最新的底部位置。
这个问题可把我们难到了,尝试MutationObserver或者去掉recalcposition的延迟都没有什么效果。
也许只能这样了?
思考的时间
我下楼抽了根烟,在考虑为什么会发生这个延迟。
React渲染机制的启示
当想到之前看过的React渲染机制的时候,猛然想到,在React中,无论如何,这些浏览器API生效的时候都在Commit阶段之后。
那么只有等到下一次React渲染的时候,新的位置才会生效。为了把这次调整操作提前,是否可以使用useLayoutEffect这个Hook,在提交到DOM之前调整其位置。
最终解决方案:useLayoutEffect
我立即回来尝试了一番,
useLayoutEffect(() => {
panelRef.current.scrollIntoView({ behavior: "instant", block: "end" });
}, [someVarToShowChoosePanel]);
这里的someVarToShowChoosePanel 是用来控制选择面板的显示与隐藏的状态。
果不其然,延迟消失了。
总结
useLayoutEffect 与普通的 useEffect 相比,最关键的区别在于它的执行时机。它会在DOM变更后、浏览器绘制前同步执行,这使得它特别适合于需要直接操作DOM并且希望避免视觉闪烁的场景。
在我们的案例中,这正是解决渲染延迟问题的关键 - 确保滚动操作发生在浏览器绘制之前,而不是之后。这种精确的时机控制是React hooks提供给我们的强大工具之一。