Published on
1557

Antd Modal对话框组件抖动问题排查

Authors
  • avatar
    Name
    小辉辉
    Twitter

前言

项目里一直用的React框架配合最新的Antd5.x版本进行开发,有一次产品反馈一个问题,某某对话框关闭的时候会明显的闪一下,而且这现象在好多对话框下面都有。

看到这个问题,第一反应就是先复现这个情况,连续测试了项目中多处使用对话框的地方,发现的确有少部分对话框在关闭的时候会有抖动,这个效果一时半会描述不出来,给人的感觉就是关闭了以后,有那么一瞬间又重新打开了,之后再关闭。

最奇怪的是只有特定的几个场景的对话框会有这个问题,大部分情况下都是正常的,而且有问题的地方也不是100%必现。

原因排查

带着这些个疑问就有了这篇文章,决定先自己去找找原因,到底是项目上的代码用的不对,还是antd自身的modal组件有问题。

首先我的思路就是先找到复现步骤,对比了好的对话框和有问题对话框的写法,好的对话框有处很明显的配置destroyonclose,我往有问题的对话框加了这个配置之后,嗯,抖动的问题的确消失了,但是这样一来也带来了一个新的问题,那就是对话框内容的交互状态不能保留。例如对话框内部输入了用户的内容,一般这部分内容我们都是在组件内部收集的,不会托管到最外层的state上面,一旦使用了destroyonclose,可以理解为每次打开对话框,用户上次状态操作的state都会被重置。

既然不行,只能硬着头皮再往下查原因了。这次我打算先排除是项目代码本身的原因,我把有问题modal框内部的代码拷贝了一份,直接放到了整个项目的入口处,模拟对应的交互操作,结果确让我惊呆了,没有问题,连续着操作了好多次,一次都没有出现闪动的问题。

到了这里,已经开始意识到这个问题有点复杂了,可能不得不看antd对应modal组件的代码了,先是看了下对话框渲染后的结构,从外向内大致上是这么几层:最外层遮罩-> 最外层容器 -> 内部容器 -> 内部单个组件。我往中间的两个容器分别加了属性修改断点,开始操作,结果就发现了下面的情况:

在关闭逻辑发生后,先是内部容器发生了多次类名的修改,基本上能确定是动画交互的需要,走到最后一个动画是scale:0; opacity,我们可以理解为就是对话框消失的作用,照理说走完这个应该就要讲内部容器样式设置为display: none的,而实际情况并没有添加任何样式,反而类名又恢复到了关闭操作之前的值。这个时候我们就明显看到了对话框从原本消失的状态又显示出来了,再往下走,终于外部容器有了一个样式的改动,就是上面我们想要的display: none,只是这个样式是作用在最外层的容器上面。

到了这里,整个闪现发生的逻辑已经很清晰了,再寻着打断点时候对应的antd源码,对这个modal对话框组件的整体渲染也慢慢梳理出来了:

  1. 先是antd自身的modal组件
  2. 内部调用了rc-dialog
  3. rc-dialog在最外层套用了一侧rc-motion动画组件
  4. 在rc-motion组件内部传入了上述rc-dialog中的body组件,可大致看作是我们传给Modal的组件
  5. rc-dialog会往rc-motion传入一个panelchange的事件
  6. rc-motion在关闭时会依次执行动画,在最后离开动画触发的时候,会改变自身维护的一个state状态open
  7. rc-motion内部会监听上述提到的open状态,如果发现有变化,会调用上面的panelchange事件
  8. panelchange事件,负责将display:none的样式加到容器的最外层

分析到这里,那么问题来了,为什么单独将有问题的Modal组件放置在真个项目的入口就没有问题了呢?

我们通过上述的流程分析,可以大致猜测问题应该出现在panelchange调用的时机,如果这个监听事件调用超过16ms,即一帧运行需要的时候,那么我们会明显看到对话框会恢复为最初大小,而外层的隐藏样式还没有作用,如果事件低于16ms,那么自然而然我们就看不到对话框显示了。

为了验证这个猜测,我在原有有问题的地方,分别输出了用时,最后发现有问题的地方监听时间达到了30ms左右,远远超过了一帧需要的16ms,而提到最外层的modal组件只需要短短的8ms。

下面再看下具体的代码

const {onPanelChange} = props;
const [open, setOpen] = useState(false);

useEffect(()=>{
    // 问题就出现在这里
    // 从onMotionEnd方法调用
    // 到实际被监到的时间远远大于了16ms
    if (!open) {
        onPanelChange(false);
    }
}, [open])

const onMotionEnd = () => {
    setOpen(false);
}

解决方案

关于上面监听触发时间的问题,我一时半会也没有什么思路,后面有时间再去深究吧。

当下针对这个问题先去antd社区看看这个问题有没有被修复,看了rc-motion组件,发现新版本已经兼容了这个问题,具体做法是在动画结束的时候,会同步设置当前内部的内容一个 display: none的样式,这样就不需要在依赖外部样式隐藏了,看到这里,马上给项目升级rc-motion组件到最新版本,最后对话框抖动的问题成功解决。