pro-table 高性能表格 - 更新日志
1.0.9(2026-05-07)
性能优化
LayerDrawHelper 消除每帧 O(N) 分配
- types.ts:
TableLayout 新增 layerCols: IColumnGroup[]、layerColOffsets: number[]、layerColKeyMap: Map<string, number> 三个字段——按 fixed 分组后的合并叶列数组、列宽前缀和及 key→索引 Map,列变化时重建一次,帧内直接复用
- hooks/useTableLayout.ts:
computeTableLayout 在列分组计算完成后一次性构建 layerCols/layerColOffsets/layerColKeyMap 并嵌入返回对象
- layers/LayerDrawHelper.ts:构造函数改为直接引用
layout.layerCols/layout.layerColOffsets/layout.layerColKeyMap,移除 buildColOffsets import;消除每帧 2× O(列数) 的数组分配与 Map 构建开销(Overlay body + fixedTop 两路各触发一次)
drawText / drawSmoothLine / drawPolyline / drawArea 行列双向自动裁剪
- layers/LayerDrawHelper.ts:
drawText 改为复用 _isInVisibleRange 做行列双向可见性检测——以 text.length × fontSize 作为文字宽度的严格保守上界(CJK ≈ 1× fontSize/char,ASCII ≈ 0.55×,取 1× 保证无漏判),消除旧版仅有 Y 轴检测的遗漏;canvas clip 保障像素级正确性,_isInVisibleRange 仅对完全位于主区可见列窗口外的文字跳过,固定列区域恒允许;至此 LayerDrawHelper 全部 draw* 方法均实现行列双向裁剪
- layers/LayerDrawHelper.ts:新增私有方法
_trimPoints(points, buffer)——检测点集主方向:Y 单调递增时按可见行像素窗口(_visY1/_visY2)裁剪;X 单调递增且整体在主区内时按可见主区列像素窗口(_visMainX1/_visMainX2)裁剪;两端各保留 buffer 个边界点供 Catmull-Rom 切线计算;跨固定列或双向非单调时不裁剪,保证正确性
- layers/LayerDrawHelper.ts:
drawSmoothLine、drawPolyline、drawArea 在发送 Canvas 命令前调用 _trimPoints(points, 1),将 JSBridge 命令量从 O(总行/列数) 降至 O(可见行/列数);500 行趋势线场景约减少 94% 的 bezierCurveTo/lineTo 调用,App 平台滑动帧率显著提升
1.0.8(2026-05-06)
性能优化
OverlayLayer 自定义绘制全局可见性裁剪
- layers/LayerDrawHelper.ts:新增完整的行列双向可见性裁剪系统——构造时从
ILayerDrawContext.virtual 预算可见行范围(_visRowStart/_visRowEnd)、主区可见列范围(_visColStart/_visColEnd)、可见区像素边界(_visY1/_visY2、_visMainX1/_visMainX2)以及主区全范围像素边界(_mainAreaX1/_mainAreaX2),帧内常量,零额外计算
- layers/LayerDrawHelper.ts:
drawCell* 系列(drawCellBg/drawCellCircle/drawCellText/drawCellBadge)和 drawColBg/drawRowBg 均加入行列索引级早退检测,不在可见窗口内的单元格直接跳过,避免无效 Canvas API 调用
- layers/LayerDrawHelper.ts:
drawCircle/drawRect/drawRoundRect/drawHLine/drawVLine/drawLine 加入像素级包围盒检测(_isInVisibleRange),包围盒完全在可见区域外则跳过
- layers/LayerDrawHelper.ts:
_isInVisibleRange 使用预算的 _mainAreaX1/_mainAreaX2 常量,热路径消除每次调用时的 fixedLeftPx/fixedRightPx 数组查找和算术计算
StickyHeaderLayer 表头列虚拟化
- layers/StickyHeaderLayer.ts:min 区单元格绘制和边框循环均增加列虚拟化过滤——基于
virtual.colStart/colEnd 转换为全局叶列索引范围,跳过完全在可见区域外的单元格,含 colSpan>1 的分组表头仅在任意部分可见时才绘制;100 列只有 10 列可见时每帧 Canvas API 调用量降低约 90%
- layers/StickyHeaderLayer.ts:查询
layout.leafColOffsets 直接复用全量叶列前缀和,替代每帧重新分配和填充 colOffsets 数组
TableLayout 增加共享字段
- types.ts:
TableLayout 新增 leafColOffsets: number[]——全量叶列宽度前缀和(fixedLeft → main → fixedRight),长度 = leafColumns.length + 1,供 StickyHeaderLayer 等直接复用
- types.ts:
TableLayout 新增 hasSpan: boolean——布局计算时一次性检测主区是否存在 span 函数,避免 BodyLayer 每帧调用 Array.some
- hooks/useTableLayout.ts:
computeTableLayout 在已算好 mainColOffsets 后延伸计算 leafColOffsets 和 hasSpan,一并嵌入返回对象
BodyLayer 消除每帧高频遍历
- layers/BodyLayer.ts:
mainContentW 由每帧 Array.reduce 改为直接取 layout.mainColOffsets[mainCols.length](O(N) → O(1))
- layers/BodyLayer.ts:hover 列索引查找由每帧
Array.findIndex 改为 Map.get()(O(N) → O(1)),Map 仅在 mainCols 引用变化时重建
- layers/BodyLayer.ts:
hasSpans 由每帧 Array.some 改为读取 layout.hasSpan 布尔缓存
hitTest 列命中改二分查找
- hooks/useHitTest.ts:
hitTestLeafColIndex 和 hitTestColKey 的主区列命中由 O(N) 线性扫描改为利用 mainColOffsets 的二分查找(O(N) → O(log N)),优化高频 mousemove 场景下的列定位性能
BodySpanMask 跨帧缓存
- layers/BodyLayer.ts:引入
_spanMaskCache 字段缓存上一帧的 BodySpanMask;当 data 与 mainCols 引用不变且当前 rowStart/rowEnd 完全在上次扫描范围内时直接复用,已避免每帧 O(可见行 × 所有列)的 span() 函数重复调用
Bug 修复
OverlayLayer payload 遗漏新增属性导致崩溃
- layers/OverlayLayer.ts:手动构建的 payload 对象补充转发
visibleRowRange/visibleColRange(getter)、isRowVisible/isColVisible,修复因属性未转发导致 payload.visibleRowRange.start 抛出 TypeError: Cannot read property 'start' of undefined 的崩溃
drawCellBadge 测量宽度前未设置字号
- layers/LayerDrawHelper.ts:
drawCellBadge 在调用 measureText 前加入 this._r.setFontSize(fontSize),修复沿用上次渲染调用遗留字号状态导致 badge 宽度错误的问题
API 新增
ILayerDrawHelper 新增可见范围查询字段
- types.ts:
ILayerDrawHelper 新增 visibleRowRange: Readonly<{ start: number; end: number }>——当前可见行范围,可用于跳过不可见行的 JS 侧自定义计算
- types.ts:
ILayerDrawHelper 新增 visibleColRange: Readonly<{ start: number; end: number }>——当前可见主区列范围(绝对索引),fixedLeft/Right 列不受此约束
- types.ts:
ILayerDrawHelper 新增 isRowVisible(rowIndex: number): boolean——判断行是否在可见窗口内,draw* 系列已内置此判断,通常无需手动调用
- types.ts:
ILayerDrawHelper 新增 isColVisible(colIndex: number): boolean——判断列是否在可见窗口内,fixedLeft/Right 列恒返回 true
文档
- readme.md:
ILayerDrawHelper 接口代码块同步新增 4 个字段;坐标查询 API 说明表补充对应条目;单元格便捷系列说明表上方新增内置可见性保护说明
1.0.7(2026-05-06)
API 新增
自定义表头单元格渲染(renderHeader)
- types.ts:新增
HeaderCellRenderParams 接口——包含 ctx、x/y/width/height、title、column、isLeaf、align、fontSize、paddingX、textColor 及辅助方法 drawText(text, color?)、drawSortArrow()、fillBackground(color, alpha?)、drawRect(x,y,w,h,color,alpha?)、drawCircle(cx,cy,r,color,opts?)、drawRoundRect(x,y,w,h,radius,fillColor,strokeColor?,lineWidth?);所有绘制方法底层复用 drawCirclePrimitive / roundRectPath 原语,与 ILayerDrawHelper API 风格保持一致,自动抹平 H5 / 小程序差异,无需直接操作 ctx
- types.ts:
ColumnNode 新增可选字段 renderHeader?: (params: HeaderCellRenderParams) => void——有此函数时,该单元格的默认文字和排序箭头均不绘制,完全由回调接管;对父级分组列和叶子列均有效
- types.ts:
HeaderCellMeta 新增 colNode?: ColumnNode 字段,携带对应列配置引用,供绘制层读取 renderHeader
- useTableLayout.ts:
buildHeaderMeta 中叶子列和父级列的 headerGrid.push 均填入 colNode 引用
- StickyHeaderLayer.ts:main / fixedLeft / fixedRight 三个绘制循环均加入
renderHeader 分支:若 cell.colNode.renderHeader 存在,则 r.save() 后调用回调(异常 catch 打印错误),再 r.restore() 并 continue 跳过默认绘制路径;renderHeader 内部调用 drawSortArrow() 方可显示排序指示器,否则不自动绘制
代码质量
渲染回调类型重构:CellRenderBase 公共基类 + drawCircle 签名统一
- types.ts:新增
CellRenderBase 接口,提取 CellRenderParams 与 HeaderCellRenderParams 的公共字段——ctx、x/y/width/height、align、fontSize、paddingX、textColor、drawText,以及本次统一后的 drawCircle(cx, cy, r, color, opts?);两个接口均改为 extends CellRenderBase
- types.ts:
CellRenderParams.drawCircle 签名由旧式便捷签名 (fillColor, opts?) 改为显式坐标签名 (cx, cy, r, color, opts?),与 HeaderCellRenderParams.drawCircle 及 ILayerDrawHelper.drawCircle 三者完全一致,消除 API 命名相同但语义不同的歧义
- BodyLayer / fixedCellHelper:
drawCircle 闭包实现更新为 (cx, cy, radius, color, opts) => drawCirclePrimitive(r, cx, cy, radius, color, opts, fontSize),不再自动计算单元格内切圆坐标
1.0.6(2026-05-06)
Bug 修复
render 回调中 drawText 忽略 cellOverflow
- BodyLayer / fixedCellHelper:修复
CellRenderParams.drawText(text, color) 始终走单行 fillText 逻辑的问题——当列配置了 cellOverflow: 'wrap' 或 'ellipsis' 时,在 render 回调中调用 drawText 自定义文字颜色会导致多行折行/省略截断完全失效;现在 drawText 内部与非 render 路径保持一致,按 col.cellOverflow 分别走 ellipsis(truncateWithEllipsis + clip)、wrap(wrapTextLines 分行居中)、clip(默认单行 clip)三个分支
API 新增
ILayerDrawHelper / OverlayDrawPayload 扩展
- types.ts:
ILayerDrawHelper 新增坐标查询 API——getRowCount()、getColCount()、getColBounds(colIndex)、getRowBounds(rowIndex),供 Overlay 回调精确定位行列区域
- types.ts:
ILayerDrawHelper 新增绘制辅助 API——drawLine(x1,y1,x2,y2,color,lineWidth?)(任意直线)、drawRect(x,y,w,h,color,alpha?)(填充矩形,支持透明度)、measureText(text,fontSize?)(文字宽度测量)、drawColBg(colIndex,color,alpha?)(整列背景)、drawRowBg(rowIndex,color,alpha?)(整行背景)
- LayerDrawHelper.ts:实现上述所有新增接口方法;新增私有
_getRegionHeight() 计算当前区域总高度(body 用 rowOffsets 前缀和,固定行区域用行数 × cellHeight)
- OverlayLayer.ts:
OverlayDrawPayload 补充以上新方法绑定,bodyOverlayDraw / fixedTopOverlayDraw / fixedBottomOverlayDraw 回调均可使用完整 API
代码质量
绘制原语提取与复用
- utils.ts:新增
drawCirclePrimitive(r, cx, cy, radius, color, opts?, fontSize?) 导出函数——圆形绘制唯一原语,供 LayerDrawHelper、BodyLayer._drawCell、fixedCellHelper._drawFixedCell 三处共用,消除完全相同的 20 行 save/arc/fill/stroke/text/restore 重复逻辑
- utils.ts:新增
roundRectPath(ctx, x, y, w, h, r) 导出函数——圆角矩形路径唯一原语,统一替代原先分散在 LayerDrawHelper、TooltipLayer、utils.ts(drawActionButtons 内部)三处各自内联的相同路径代码;原私有 roundRect 函数删除
- LayerDrawHelper:
drawCircle 方法改为调用 drawCirclePrimitive,drawRoundRect 方法改为调用 roundRectPath,各删除约 20 行内联路径逻辑
- BodyLayer:
_drawCell 中的 drawCircle 闭包改为调用 drawCirclePrimitive
- fixedCellHelper:
_drawFixedCell 中的 drawCircle 闭包改为调用 drawCirclePrimitive
- TooltipLayer:删除本地私有
drawRoundRect 函数(20 行),改为 import { roundRectPath } 直接使用
无用导入清理
- BodyLayer:移除从未使用的
import type { ClickAreaRegistry } 及 import type { ProTableOptions }
- fixedCellHelper:移除从未使用的
import type { ProTableOptions }
- FixedLeftLayer:移除从未使用的
import type { ProTableOptions } 及 import { getCellTextPosition, truncateWithEllipsis, wrapTextLines }(文本渲染全部委托给 _drawFixedCell,FixedLeftLayer 自身不直接调用这三个函数)
1.0.5(2026-05-06)
Bug 修复
资源泄漏 / 生命周期
- pro-table.vue(H5):修复
onUnmounted is called when there is no active component instance 警告——onMounted 为 async 函数,原先在 await nextTick() 与 await initRenderer() 之后调用 onUnmounted(_teardownKeyDown),此时 Vue 内部活跃组件实例指针已被清空,导致钩子无法关联;将 _handleKeyDown 定义及 onUnmounted 注册提升至 setup 顶层,document.addEventListener 仍在 onMounted 内挂载,彻底消除警告并保证卸载时正确移除监听器
1.0.4(2026-05-01)
文档
- uni_modules/pro-table/readme.md:增加赞赏路径,移动体验码展示位置。
1.0.3(2026-04-28)
文档
- uni_modules/pro-table/readme.md:将 logo 和微信小程序体验二维码的图片路径由相对路径(
./static/)改为绝对 URL(https://proui.xsbcme.cn/static/),修复在 uniapp 插件市场页面图片无法显示的问题
1.0.2(2026-04-28)
Bug 修复
单元格合并(Span)
- BodyLayer / FixedLeftLayer / FixedRightLayer:修复 rowspan 合并格 hover 高亮仅覆盖起始行的视觉 Bug——新增
BodySpanMask.getHoverRowRange() 方法,hover 高亮现在正确覆盖合并格的完整行高区域;同时兼容"指针落在被 span 覆盖行"与"指针落在 span 源行"两种情形
单元格选区
- usePointerEvents(触摸端):修复长按建立选区后无法取消的问题——快速点击(未触发长按)若存在选区,现在会清除选区;与长按建立选区形成对称操作
- usePointerEvents(隐式 Bug):修复点击表头时
colIdx >= 0 条件成立导致在第一行产生错误选区的问题;新增 isHeaderArea 检测,表头点击不再进入选区建立逻辑
- usePointerEvents(鼠标端):点击表头区域或 body 空白区域(无列命中)时,自动清除已有选区并进入滚动模式;原逻辑仅切换模式但保留了旧选区
- usePointerEvents:修复
_isSingleCell() 始终返回 true 的逻辑错误(未实际读取 SelectionRange),导致选区存在时长按仍触发扩选而非正常选区操作
资源泄漏 / 生命周期
- usePointerEvents:新增
destroy() 方法,组件卸载时取消未完成的惯性滚动 requestAnimationFrame 及长按 setTimeout,消除潜在内存泄漏与 unmount 后 state 写入
- useActionButtons:新增
destroy() 方法,组件卸载时清除按钮按压效果计时器
- useRenderer:新增
_destroyed 守卫标志,teardown() 设置后 scheduleRender() 立即返回,防止 App 平台异步 ctx.draw() 回调在组件已 unmount 后继续触发渲染(ghost render)
- pro-scrollbar.vue(H5):新增
onUnmounted 清理,组件卸载时移除可能残留的 window 鼠标拖拽监听器,防止内存泄漏
- pro-table.vue(H5):新增
document 级 keydown 监听,按 Escape 键可取消当前选区;onUnmounted 时自动移除监听器
错误边界
- LayerManager:
layer.draw() 增加 try-catch 保护,单个 Layer 抛出异常时仅记录错误,不影响其它 Layer 继续渲染
- BodyLayer:
col.render() 增加 try-catch 保护,用户自定义渲染函数异常时仅记录错误,表格继续正常绘制
- fixedCellHelper:固定行用户
renderFn 增加 try-catch 保护,与 BodyLayer 保持一致
- OverlayLayer:
_callback() 增加 try-catch 保护,用户 Overlay 回调异常时仅记录错误,不中断渲染
数据逻辑
- useTooltip:
scheduleAutoHide() 新设计时器前先清除旧定时器,防止多次快速触发累积定时器导致 tooltip 提前消失
- useTreeData(懒加载):修复
loadChildren 并发竞态——对处于 __loading__ 状态的节点忽略重复展开点击,防止重复 HTTP 请求
- useTreeData(懒加载):
loadChildren 抛出异常时不再静默吞掉——自动记录错误日志并调用 TreeConfig.onLoadError 回调(新增 API);同时自动收起展开状态
- usePagination:
pageCount 减少时(如删除数据后条数减少)自动将 currentPage 夹紧到合法范围并触发 onPageChange,防止分页越界导致数据列表空白
- usePagination:外部传入
pagination.current 时同样执行页码夹紧,防止初始页码超出总页数范围
- useSort(树形模式):树形模式下
toggleSort() 直接返回,不再更新排序状态,保持树形层级顺序不被破坏
- pro-table.vue(树形 + 排序状态):树形模式下向 StickyHeaderLayer 传入空排序状态
{ key: '', order: null },确保列头排序箭头不显示
- useTableLayout(wrapTextLines):
maxWidth <= 0 时直接返回单行,防止负数宽度下每字符单独成行,造成行高计算异常
性能优化
- useTableLayout(4.1):
cellOverflow='wrap' 模式下新增行高 WeakMap 缓存——以 (data 数组引用, wrap 列宽签名) 为键缓存计算结果;data 整体替换时自动失效,列宽未变化时直接命中缓存,1000+ 行自动换行表格的布局重算开销降至接近零
- useLayoutManager(4.2):拆分原
watch([getColumns, getMergedOptions], ..., { deep: true })——getColumns 保留 deep: true(响应列属性原地修改);getMergedOptions 改为浅监听(computed 每次返回新对象引用,无需深遍历),消除 100+ 列配置时的深度遍历开销
- useTreeData(4.3):
flatData 从 computed 改为 shallowRef + 增量更新策略——展开/折叠单个节点时直接在 flatData 数组中 splice 插入/删除子树(O(children) 而非 O(all_rows));数据替换、树模式切换、懒加载完成及 expandAll/collapseAll 仍走全量重算;外部直接修改 expandedKeys 也通过 flush:'sync' watcher 触发全量重算,保证正确性
- useRenderer + usePointerEvents(4.4):新增
scheduleHoverRender() — hover 专用局部重绘调度;handleHoverMove 和 handleHoverLeave 改用 scheduleHoverRender():仅标脏 body / fixed-left / fixed-right / selection 四个层,跳过表头、阴影、Overlay 等静态层,不清屏直接覆盖绘制,小程序端每帧 Canvas JSBridge 调用量大幅减少;全量 scheduleRender() 始终优先(若队列已有全量帧,hover 帧自动降级)
API 新增
- TreeConfig.onLoadError(
types.ts):树形懒加载 loadChildren 失败时的错误回调,签名 (error: unknown, row: RowData) => void,可在此回调中提示用户或上报错误
1.0.1(2026-04-23)
性能优化
- UniLegacyRenderer:新增 JSBridge 样式缓存(
fillStyle、strokeStyle、lineWidth、textAlign、fontSize、textBaseline),重复值不再触发 IPC 调用,单帧 JSBridge 消息量减少约 40–50%
- UniLegacyRenderer:新增
save() / restore() 镜像栈,restore() 后自动恢复全部缓存字段,确保缓存始终与渲染器状态同步
- BodyLayer:将
setTextAlign() 移至 save() 之外,使对齐方式跨单元格复用缓存,进一步减少 JSBridge 调用
- useRenderer:App 路径跳过
_checkMpResize()(UniApp 内部自行管理 canvas 尺寸),每帧减少一次额外查询
Bug 修复
- pro-scrollbar:修复 App WebView 被动监听器(
cancelable = false)下无条件调用 preventDefault() 导致的 "Ignored attempt to cancel a touchstart event" / "touchmove event" 控制台警告;改为 _tryPreventDefault() 在调用前检查 e.cancelable !== false
- MpEventAdapter:修复 App-vue 与小程序坐标系差异——小程序 touch 含
x/y(元素内坐标)直接使用;App-vue 仅有 clientX/Y(视口坐标),需减去 canvas 视口偏移(由 setCanvasOffset() 注入);修复文件编码为 UTF-8(无 BOM)
- AppCanvasPlatformAdapter / MpCanvasPlatformAdapter:新增
requestAnimationFrame 支持,帧调度与系统 vsync 对齐,setTimeout/nextTick 降级兜底
- useRenderer:小程序新 API 路径(
nativeCanvas.getContext)传入 { willReadFrequently: true } 选项,消除相关警告;失败时自动降级不带选项重试
功能改进
- 示例首页(
pages/index/index.vue)完全重构:
- 新增品牌头部(logo + 渐变背景 + Canvas 徽标 + 作者信息 + 统计栏)
- 卡片改为按分组渲染,12 个碎分组合并为 7 个语义分组(基础功能 / 表头 & 结构 / 大数据 & 性能 / 单元格渲染 / 选择 & 排序 / 高级交互 / 数据 & API)
- 布局改为 JS 预切行(每行固定 3 个)+ 每行独立
flex 容器,flex: 1 严格三等分,彻底规避 App/小程序 WebView flex-wrap 百分比计算不一致问题
card-name 单行截断,card-desc 最多两行截断,card 加 overflow: hidden,防止长内容撑宽卡片破坏列宽
- 新增页脚版权信息
1.0.0(2026-04-21)
首次发布
- 基于 Canvas 2D 的高性能表格组件,支持 H5 和微信小程序
- 支持固定列(
fixed: 'left' | 'right')与固定行(fixedRows)
- 支持列宽 / 行高拖拽调整(
useColRowResize)
- 支持虚拟滚动:行虚拟(超过阈值自动开启)、列虚拟(列数 > 10 自动开启)
- 支持单元格合并(
options.spanMethod)
- 支持多级表头(
ColumnNode.children)
- 支持行选择:复选框 / 单选(
rowSelection)
- 支持区域框选(
enableSelection)
- 支持排序(
ColumnNode.sortable,受控 / 非受控模式)
- 支持树形数据展开 / 折叠(
treeProps)及懒加载
- 支持分页配置(
pagination)
- 支持 Tooltip 悬浮提示(
ColumnNode.tooltip)
- 支持自定义单元格渲染(
ColumnNode.cellRender)
- 支持操作按钮列(
ColumnNode.actionButtons)
- 支持加载状态(
loading)与空状态(EmptyLayer)
- 支持条纹行、悬停高亮、列悬停高亮
- 支持文本溢出处理:截断 / 省略号 / 自动折行(
cellOverflow)
- 支持自定义 Overlay 绘制(
bodyOverlayDraw / fixedTopOverlayDraw / fixedBottomOverlayDraw)
- 暴露 Ref 方法:
scrollTo、scrollToRow、scrollToColumn、getSelectionRange、clearSelection、getSelectedRowKeys、clearRowSelection、getSortState、resetSort、getExpandedKeys、setExpandedKeys、redraw