关于 Antd Table 报错 ”ResizeObserver loop completed with undelivered notifications“

10,179 阅读5分钟

背景

在 sentry 里发现线上发现一个报错 ”ResizeObserver loop completed with undelivered notifications“ ,于是调查了一下,大概是因为 antd 的 table 组件(v4.20.x)(其实下拉框组件也会触发类似的报错)。

调研

先是谷歌了一下,搜索 ”ResizeObserver loop completed with undelivered notifications“ 时发现大部分文章是在讨论 “ResizeObserver loop limit exceeded” 这个报错,但在 developer.mozilla.org/en-US/docs/… 上找到了 “ResizeObserver loop completed with undelivered notifications” 报错的一些描述:

Implementations following the specification invoke resize events before paint (that is, before the frame is presented to the user). If there was any resize event, style and layout are re-evaluated — which in turn may trigger more resize events. Infinite loops from cyclic dependencies are addressed by only processing elements deeper in the DOM during each iteration. Resize events that don't meet that condition are deferred to the next paint, and an error event is fired on the Window object, with the well-defined message string:

ResizeObserver loop completed with undelivered notifications.

Note that this only prevents user-agent lockup, not the infinite loop itself. For example, the following code will cause the width of divElem to grow indefinitely, with the above error message in the console repeating every frame:

const divElem = document.querySelector("body > div");

const resizeObserver = new ResizeObserver((entries) => {
  for (const entry of entries) {
    entry.target.style.width = entry.contentBoxSize[0].inlineSize + 10 + "px";
  }
});

window.addEventListener("error", function (e) {
  console.error(e.message);
});

As long as the error event does not fire indefinitely, resize observer will settle and produce a stable, likely correct, layout. However, visitors may see a flash of broken layout, as a sequence of changes expected to happen in a single frame is instead happening over multiple frames.

这里解释了这个报错的原因:在页面绘制的时候,页面突然发生调整大小的事件,导致了样式和布局都需要重新评估,这个调整大小导致的布局变化,将延迟到下一帧来绘制。

然后看了一下 antdtable(因为 sentry 报错链接中均含有 table 组件)组件,它使用了 rc-table ,而 rc-tablerc-resize-observer 导入了 ResizeObserver ,看下它的使用:

if (horizonScroll) {
  fullTable = <ResizeObserver onResize={onFullTableResize}>{fullTable}</ResizeObserver>;
}

  const horizonScroll = (scroll && validateValue(scroll.x)) || Boolean(expandableConfig.fixed);

可以看到,当 table 设置了 scroll.x 或设置了固定展开行时,就会使用 ResizeObserver 包裹着。 ResizeObserver 底层是使用了 resize-observer-polyfill ,从库的名字也可以看得出来,这是一个 resize-observer 功能的 polyfill

// Returns global object of a current environment.
export default (() => {
    if (typeof global !== 'undefined' && global.Math === Math) {
        return global;
    }

    if (typeof self !== 'undefined' && self.Math === Math) {
        return self;
    }

    if (typeof window !== 'undefined' && window.Math === Math) {
        return window;
    }

    // eslint-disable-next-line no-new-func
    return Function('return this')();
})();

export default (() => {
    // Export existing implementation if available.
    if (typeof global.ResizeObserver !== 'undefined') {
        return global.ResizeObserver;
    }

    return ResizeObserverPolyfill;
})();

这里我们可以确认的是:antd 的 table 组件(v4.20.x), 用到了 ResizeObserver 方法,这跟上面那个报错 ResizeObserver loop completed with undelivered notifications 呼应了。

有趣的是,当我运行 developer.mozilla.org/en-US/docs/… 里提到的代码,也就是 Observation Errors 里的报错代码,在控制台上看到的报错是:ResizeObserver loop limit exceeded 。这有可能跟浏览器相关,感兴趣的小伙伴可以自行尝试。

看到这儿,我就理解了,为什么我谷歌 ResizeObserver loop completed with undelivered notifications 这个报错时,却发现大量的文章都是在讨论 ResizeObserver loop limit exceeded 这个报错的原因了。

小结一下:

antd 的 table 组件确实有可能会报 ResizeObserver loop completed with undelivered notifications 这样的错误。

分析

这个最终是需要分析 rc-table 这个库,在该项目里直接搜索 ResizeObserver 这个组件,追踪链条如下:

ResizeObserver -> fullTable -> groupTableNode -> bodyTable -> Body -> data isEmpty -> ExpandedRow

ResizeObserver -> fullTable -> groupTableNode -> bodyTable -> Body -> data.length > 0 -> BodyRow -> ExpandedRow 

在找的过程中,要跟上面的分析结合起来,比如要注意 horizonScroll :

// rc-table v7.30.3 src/Body/ExpandedRow.tsx
let contentNode = children;

if (isEmpty ? horizonScroll : fixColumn) {
  contentNode = (
    <div
      style={{
        width: componentWidth - (fixHeader ? scrollbarSize : 0),
          position: 'sticky',
            left: 0,
              overflow: 'hidden',
      }}
      className={`${prefixCls}-expanded-row-fixed`}
      >
      {componentWidth !== 0 && contentNode}
    </div>
  );
}

为什么会关注到这段代码?因为这里有个 width 的设置,这里可能会被 ResizeObserver 监听到,就有可能会触发报错。

根据上面代码所示,horizonScrolltrue 且进入条件判断的话,isEmpty 的值也是 true

isEmpty 来源于:

//rc-table v7.30.3 src/Body/index.tsx
if (data.length) {
  // ...
} else {
 rows = (
  <ExpandedRow
      expanded
      className={`${prefixCls}-placeholder`}
      prefixCls={prefixCls}
      component={trComponent}
      cellComponent={tdComponent}
      colSpan={flattenColumns.length}
      isEmpty
      >
      {emptyNode}
    </ExpandedRow>
  ) 
}

//rc-table v7.30.3 src/Table.tsx
const mergedData = data || EMPTY_DATA;
const hasData = !!mergedData.length;
// Empty
const emptyNode: React.ReactNode = React.useMemo(() => {
  if (hasData) {
    return null;
  }

  if (typeof emptyText === 'function') {
    return emptyText();
  }
  return emptyText;
}, [hasData, emptyText]);

const bodyTable = (
  <Body
    data={mergedData}
    measureColumnWidth={fixHeader || horizonScroll || isSticky}
    expandedKeys={mergedExpandedKeys}
    rowExpandable={expandableConfig.rowExpandable}
    getRowKey={getRowKey}
    onRow={onRow}
    emptyNode={emptyNode}
    childrenColumnName={mergedChildrenColumnName}
    />
);

小结一下:

data 判断为空,且 horizonScroll 判断为 true 时,ResizeObserver 包裹下的 childrenwidth 可能会发生变化,这时候就有可能会触发 ResizeObserver loop completed with undelivered notifications 报错。

所以,如果我们传入非空的值的话,是不是就消除这个报错了呢?我们来实践 一下。

antd 版本大于等于 4.0 的时候

  1. 当初始化 tabledataSource 赋值空数组时:
<Table columns={[]} dataSource={[]} scroll={{ x: 100 }} />

控制台就会报错:ResizeObserver loop limit exceeded

  1. 当初始化 tabledataSource 赋值 [{}] 时:
<Table columns={[]} dataSource={[{}]} scroll={{ x: 100 }} />

控制台的报错就没了(如果想要更清楚地观察结果,可以在右侧的界面上刷新一下)。

那当 antd 版本小于 4.0 的时候,dataSource 的不同赋值会不会触发这个报错呢?

答案是:不会。

因为 antd 3.x 或以下的版本,没用上 ResizeObserver 这个功能。

Ant Design 4.0 的一些杂事儿 - Table 篇

在使用 sticky 方案后,我们的测量实际从各种渲染阶段简化到了只要在每列的宽度变化时同步一下即可。这里不得不表扬一下 ResizeObserver 同学,它是浏览原生用于监听元素内容尺寸变化的组件。在它成为正式特性之前,已经有了 polyfill 库。我们将其进行了 React 封装:rc-resize-observer ,通过 ResizeObserver 我们获得了更好的性能以及更准确的监听时机。

处理

根据以上分析,我们可知:

  1. 根据报错可知,此处报错是因为在一帧渲染中,布局大小发生了变化,从而导致了报错,并将这个改变放到下次渲染的机会去处理;
  2. 根据分析可知,触发该报错的条件是:antd 版本大于等于 4.0 且初始化时 dataSource 为空值;
  3. 根据实践来看,这个报错不会影响页面渲染且不会产生额外的副作用

所以,对于这个报错的处理,可以采取忽略的方案去应对,即关闭 sentry 对于这类错误的采集。

参考

实践

Ant Design 4.0 的一些杂事儿 - Table 篇

developer.mozilla.org/en-US/docs/…

rc-resize-observer

github.com/que-etc/res…