Published on
1449

Antd Table笔记本双指左右滚动会偶现列错位问题

Authors
  • avatar
    Name
    小辉辉
    Twitter

前言

有一天身上突然多了一个表格列错位的问题,第一反应很奇怪,因为这个表格直接用的是antd的table组件,项目也用了很久了也从来没有出现过错位问题,看到bug上的截图不得不怀疑应该是哪里出问题了。

而当自己试了试测试提供的地址,尝试多次左右拖动后,并不能复现出来,只好去请教测试询问复现步骤。

只见测试用的是笔记本电脑,人家用双指放在触摸屏上面,左右滚动起来,没一会就发现表格列错位了,具体现象就是固定表头和中间的表体数据行对不齐了。

我那时候就崩了,因为我们开发都是用的台式机,包括之前的测试,怪不得一直没有复现出来,想想这个问题可能不好改了,因为我们对antd的表格table组件滚动这一块是没有做任何定制的。

打算先看看最新的antd版本有没有这个问题,可是线上的demo操作了很久也没有复现。

没有办法只能先自己尝试解决了。

原因排查

我自己先是打算找到复现步骤,因为只有找到步骤才能在修复完以后进行验证,这也是我个人多年来改问题始终坚持的原则。

我用自己的笔记本电脑操作了起来,发现只要用触摸屏的双指操作,多次滚动以后是会出对不齐的问题,但是说不上找到一个很明确的复现步骤,因为左右滚动也是随机才会出现的。

既然没有明确的步骤,而且中间又有其它的事情要做,考虑就先放一放了。

后面再来看的时候,突然又发现复现起来要难很多了,仔细一看和之前的区别,发现现在这个版本的字体要比上个版本大,也就是说屏幕上显示的内容变少了,这个时候突然让我联想到了滚动错位是不是就是浏览器来不及响应呢?

如果是这个原因的话,我们是不是可以解决浏览器自带的cpu限制功能来强制降低运行速度,马上将速度调整为6倍速度慢,再次操作左右滚动,哎,奇迹发生了,明显能看到表头有闪动的效果,就是那种我们滚动时一卡一卡的那种效果。没滚动几下,错位就出现了。

直到这里,我意识到复现步骤是有了,下一步就是看怎么修复了。

在排查问题之前我们已经知道了antd的表格是由上下两个表格组合起来的,当滚动表体的时候的自然要手动去滚动表头的表格。那么如何快速定位到antd哪个位置来实现这个滚动呢?为了解决这个问题,我们可以给表头单独添加一个scroll的监听事件,在事件内部打印出调用堆栈,示例代码见下方:

var tableHead = document.querySelector('.ant-table-thead');

tableHead.addEventlistener('scroll', () => {
  console.trace('scroll');
})

很快,我们就定位到了antd调用滚动的源码位置,代码如下:

function forceScroll(scrollLeft: number, target: HTMLDivElement | ((left: number) => void)) {
  if (!target) {
    return;
  }
  if (typeof target === 'function') {
    target(scrollLeft);
  } else if (target.scrollLeft !== scrollLeft) {
    target.scrollLeft = scrollLeft;

    // Delay to force scroll position if not sync
    // ref: https://github.com/ant-design/ant-design/issues/37179
    if (target.scrollLeft !== scrollLeft) {
      setTimeout(() => {
        target.scrollLeft = scrollLeft;
      }, 0);
    }
  }
}

初看到这段代码,就发现了两个疑问的点:

  • 在给target.scrollLeft赋值给scrollLeft之后,居然紧接着一个if语句判断两者是否相同。
  • 最后的setTimeout没有清除的逻辑,即如果最后一次调用forceScroll方法时,没有调用setTimeout方法,必然会导致最后一次的scrollLeft设值被后面的setTimeout中的赋值给覆盖。

带着者两个疑问,分别在代码中加入调试语句,最后结果如下

  • 问题1

的确有部分情况会出现在scrollLeft赋值后和赋值的值不一样的情况,而且有个比较奇怪的现象,每次值不一样时都是一个位数很长的小数位数,这里我估计应该是浏览器内部的重设机制,在这里就暂且不往深入追究。

  • 问题2

很容易出现最后一次没有触发setTimeout的情况,这样带来的情况就是一开始提到的错位问题。

解决方案

经过上述的排查定位,下面就是解决方案了,思路也很清晰:

只要每次调用forceScroll方法时,判断是否有上次在等待中的setTimeout定时器,当然这个定时器肯定是要和target元素绑定的,如果有的话,需要先做计时器清空操作。

同样的,在每次调用setTimeout方法时也需要将返回值绑定到对应的target元素上面。

最后一步就是在setTimeout内部执行结束后也需要重置当前绑定的定时器。

最后修改后的代码如下:

function forceScroll(scrollLeft: number, target: HTMLDivElement | ((left: number) => void)) {
  if (!target) {
    return;
  }
  if (typeof target === 'function') {
    target(scrollLeft);
  } else if (target.scrollLeft !== scrollLeft) {
    target.scrollLeft = scrollLeft;

    if (target._scrollLeftTimeout) {
      clearTimeout(target._scrollLeftTimeout);
      target._scrollLeftTimeout = null;
    }

    // Delay to force scroll position if not sync
    // ref: https://github.com/ant-design/ant-design/issues/37179
    if (target.scrollLeft !== scrollLeft) {
      target._scrollLeftTimeout = setTimeout(() => {
        target.scrollLeft = scrollLeft;
        target._scrollLeftTimeout = null;
      }, 0);
    }
  }
}

修改完成后重新尝试双指左右滚动,发现滚动无比丝滑错位问题也没有出现,哈哈解决了。