问题背景

这周在帮助b哥完成他的项目前端的时候遇到了一个有趣的问题。

先介绍下背景,这是一个企业内即时通讯工具,主要的页面布局,从上往下依次包括消息列表、选择面板、操作栏、输入框。

Screenshot

初次尝试: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提供给我们的强大工具之一。