!!!###!!!title=2.6 场景树渐进加载——VisActor/VTable 社区贡献者文档!!!###!!!!!!###!!!description=---title: 2.6 场景树渐进加载 key words: VisActor,VChart,VTable,VStrory,VMind,VGrammar,VRender,Visualization,Chart,Data,Table,Graph,Gis,LLM--- ProgressProxy 是 VTable 中 Scenegraph 模块的核心子模块,主要负责处理大数据量场景下的性能优化。这个模块通过控制场景节点的创建和更新过程,确保在大数据量下仍能保持流畅的渲染和交互体验。本文旨在通过阅读源码去探究ProgressProxy的实习特点。 !!!###!!!

ProgressProxy 是 VTable 中 Scenegraph 模块的核心子模块,主要负责处理大数据量场景下的性能优化。这个模块通过控制场景节点的创建和更新过程,确保在大数据量下仍能保持流畅的渲染和交互体验。本文旨在通过阅读源码去探究ProgressProxy的实习特点。

渐进加载实现方案

控制渲染的节点数量,实现渐进式创建

代码位置:VTable\packages\vtable\src\scenegraph\group-creater\progress\proxy.ts

我们知道Vtable的首屏加载做了优化,优化首屏的思路其实和我们日常开发的优化也有共通之处,无非就是两个方向,第一是减少渲染的节点,第二是提升渲染的速度。接下来介绍怎么限制节点数量的。

在SceneProxy的构造函数里,createGroupForFirstScreen()即首屏创建时节点数量的控制逻辑主要体现在 rowLimit 和 colLimit 的配置上,这里限制了场景树里同时维护的最大行和最大列的数量,通过限制同时维护的节点数量,减少内存占用和渲染压力,确保在大数据量下仍能保持流畅的性能。

constructor(table: BaseTableAPI) {
  this.table = table;

  if (this.table.isPivotChart()) {
    this.rowLimit = Math.max(100, Math.ceil((table.tableNoFrameHeight * 2) / table.defaultRowHeight));
    this.colLimit = Math.max(100, Math.ceil((table.tableNoFrameWidth * 2) / table.defaultColWidth));
  } else if (this.table.isAutoRowHeight()) {
    this.rowLimit = Math.max(100, Math.ceil((table.tableNoFrameHeight * 2) / table.defaultRowHeight));
  } else if (this.table.widthMode === 'autoWidth') {
    this.colLimit = Math.max(100, Math.ceil((table.tableNoFrameWidth * 2) / table.defaultColWidth));
  } else {
    this.rowLimit = Math.max(200, Math.ceil((table.tableNoFrameHeight * 2) / table.defaultRowHeight));
    this.colLimit = Math.max(100, Math.ceil((table.tableNoFrameWidth * 2) / table.defaultColWidth));
  }
}    

配置的公式是例如:100, Math.ceil((table.tableNoFrameHeight * 2) / table.defaultRowHeight)

这样的计算,也就是用表格可视区域高度除行高,也就是需要实时展示的行列数,然后乘二并用100来兜底。乘二的计算的目的是设置缓冲区,加载两倍的节点。在尽量少渲染节点数的前提下,也预留了上下滚动的区间,确保在滚动时,前后都有足够的缓冲区,避免滚动时出现加载时延出现空白的情况。

渐进式创建的具体实现细节

在配置好了rowLimitcolLimit之后,那么渐进式的加载是怎么实现的呢?

经过阅读发现,渐进式渲染的核心逻辑在setYsetX,分别用于处理垂直方向/水平方向上的的滚动和渐进式渲染。主要作用是根据滚动位置动态更新表格的行,确保当前可见区域的行被正确渲染,同时释放不可见区域的行以节省内存。这里以垂直方向的SetY为例:

 async setY(y: number, isEnd = false) {
    const yLimitTop =
      this.table.getRowsHeight(this.bodyTopRow, this.bodyTopRow + (this.rowEnd - this.rowStart + 1)) / 2;
    const yLimitBottom = this.table.getAllRowsHeight() - yLimitTop;

    const screenTop = this.table.getTargetRowAt(y + this.table.scenegraph.colHeaderGroup.attribute.height);
    if (screenTop) {
      this.screenTopRow = screenTop.row;
    }

    if (y < yLimitTop && this.rowStart === this.bodyTopRow) {
      // 执行真实body group坐标修改
      this.updateDeltaY(y);
      this.updateBody(y - this.deltaY);
    } else if (y > yLimitBottom && this.rowEnd === this.bodyBottomRow) {
      // 执行真实body group坐标修改
      this.updateDeltaY(y);
      this.updateBody(y - this.deltaY);
    } else if (
      (!this.table.scenegraph.bodyGroup.firstChild ||
        this.table.scenegraph.bodyGroup.firstChild.type !== 'group' ||
        this.table.scenegraph.bodyGroup.firstChild.childrenCount === 0) &&
      (!this.table.scenegraph.rowHeaderGroup.firstChild ||
        this.table.scenegraph.rowHeaderGroup.firstChild.type !== 'group' ||
        this.table.scenegraph.rowHeaderGroup.firstChild.childrenCount === 0)
    ) {
      this.updateDeltaY(y);
      // 兼容异步加载数据promise的情况 childrenCount=0 如果用户立即调用setScrollTop执行dynamicSetY会出错
      this.updateBody(y - this.deltaY);
    } else {
      // 执行动态更新节点
      this.dynamicSetY(y, screenTop, isEnd);
    }
  }    

这里的方法本身首先计算了滚动时的顶部限制yLimitTop,底部限制yLimitBottom,并且维护当前屏幕顶部行的索引,也就是渲染的起始索引,表示从第几行开始渲染。结合前面来看,得到了渲染的起始索引和被限制渲染的的最大行数,就能从海量的渲染数据里切片出我们渲染的局部数据。

得到了渲染的起始坐标后,再通过源码里的update开头的一些更新方法实现渲染

动态更新

代码位置:VTable\packages\vtable\src\scenegraph\group-creater\progress\update-positon

dynamicSetY方法会根据滚动位置动态加载或卸载行,确保当前可见区域的行被正确渲染,同时释放不可见区域的行以节省内存。

export async function dynamicSetY(y: number, screenTop: RowInfo | null, isEnd: boolean, proxy: SceneProxy) {
  if (!screenTop) {
    return;
  }
  const screenTopRow = screenTop.row;
  const screenTopY = screenTop.top;

  let deltaRow;
  if (isEnd) {
    deltaRow = proxy.bodyBottomRow - proxy.rowEnd;
  } else {
    deltaRow = screenTopRow - proxy.referenceRow;
  }
  move(deltaRow, screenTopRow, screenTopY, y, proxy);
  if (isEnd) {
    const cellGroup = proxy.table.scenegraph.highPerformanceGetCell(proxy.colStart, proxy.rowEnd, true);
    if (cellGroup.role === 'cell') {
      const deltaY =
        cellGroup.attribute.y +
        cellGroup.attribute.height -
        (proxy.table.tableNoFrameHeight - proxy.table.getFrozenRowsHeight() - proxy.table.getBottomFrozenRowsHeight()) -
        y;
      proxy.deltaY = -deltaY;
      proxy.updateBody(y - proxy.deltaY);
    }
  }
  // proxy.table.scenegraph.updateNextFrame();
}    

dynamicSetY 方法的设计核心在于通过动态调整表格的行渲染范围,实现高效、流畅的渐进式渲染。其原理基于滚动位置的实时计算和行偏移量的动态更新,确保当前可见区域的行被正确渲染,同时释放不可见区域的行以节省内存。方法首先通过 screenTop 获取当前屏幕顶部行的信息,并根据 isEnd 参数判断是滚动中还是滚动结束,从而计算行偏移量 deltaRow 。接着,调用 move 方法,根据行偏移量动态调整表格的行渲染范围,确保表格内容与滚动位置同步。在滚动结束时,方法会进一步计算垂直偏移量 deltaY ,并更新 body 组的坐标,确保表格内容完整显示。在大数据量滚动时,ProgressProxy模块会动态更新场景节点,滚动期间,按照滚动的方向,一部分单元格会更新其在场景树中的位置,并更新单元格内的图元,完成滚动效果。

总结

我们总结出VTable 能够在大数据量场景下仍保持高性能和流畅的用户体验的秘密在于通过 ProgressProxy 模块控制表格只渲染可视范围内的数据量,并通过不断计算动态更新交互、滚动下的数据更新,也是表格组件性能优化的核心模块之一。

本文档由以下人员修正整理

玄魂