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

背景

在 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:

javascript 复制代码
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 ,看下它的使用:

react 复制代码
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

javascript 复制代码
// 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 这个组件,追踪链条如下:

javascript 复制代码
ResizeObserver -> fullTable -> groupTableNode -> bodyTable -> Body -> data isEmpty -> ExpandedRow

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

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

react 复制代码
// 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 来源于:

react 复制代码
//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 赋值空数组时:
react 复制代码
<Table columns={[]} dataSource={[]} scroll={{ x: 100 }} />

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

  1. 当初始化 tabledataSource 赋值 [{}] 时:
react 复制代码
<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...

相关推荐
光影少年2 小时前
usemeno和usecallback区别及使用场景
react.js
吕彬-前端6 小时前
使用vite+react+ts+Ant Design开发后台管理项目(二)
前端·react.js·前端框架
小白小白从不日白6 小时前
react hooks--useCallback
前端·react.js·前端框架
恩婧6 小时前
React项目中使用发布订阅模式
前端·react.js·前端框架·发布订阅模式
程序员小杨v17 小时前
如何使用 React Compiler – 完整指南
前端·react.js
谢尔登8 小时前
Babel
前端·react.js·node.js
卸任9 小时前
使用高阶组件封装路由拦截逻辑
前端·react.js
清汤饺子11 小时前
实践指南之网页转PDF
前端·javascript·react.js
霸气小男11 小时前
react + antDesign封装图片预览组件(支持多张图片)
前端·react.js
小白小白从不日白12 小时前
react 组件通讯
前端·react.js