Canvas高性能Table架构深度解析
表格组件是数据展示的核心组件之一。传统的DOM表格在处理大量数据时往往面临性能瓶颈,而基于Canvas的虚拟表格则能够在保持流畅交互的同时处理海量数据。
最近刷论坛发现了一个小而美的基于canvas的表格库(e-virt-table),于是研究了一下具体实现原理。
整体架构设计
核心类结构
EVirtTable作为整个表格系统的入口类,采用了模块化的设计思路:
export default class EVirtTable {
private scroller: Scroller; // 滚动控制
private header: Header; // 表头管理
private body: Body; // 表体渲染
private footer: Footer; // 表尾处理
private selector: Selector; // 选择器
private autofill: Autofill; // 自动填充
private tooltip: Tooltip; // 提示框
private editor: Editor; // 编辑器
private empty: Empty; // 空状态
private overlayer: Overlayer; // 覆盖层
private contextMenu: ContextMenu; // 右键菜单
private loading: Loading; // 加载状态
ctx: Context; // 上下文管理
}
这种设计将复杂的表格功能拆分为独立的模块,每个模块专注于特定的职责
高性能优化策略
1. 帧调度与绘制节流
采用了智能的绘制调度机制来避免不必要的重绘:
// 节流绘制表格
this.ctx.on('draw', throttle(() => {
this.draw();
}, () => this.ctx.drawTime));
// 节流绘制视图
this.ctx.on('drawView', throttle(() => {
this.draw(true);
}, () => this.ctx.drawTime));
draw 方法使用 requestAnimationFrame 进行帧同步,并将绘制流程和自动高度计算拆分到不同帧执行:
draw(ignoreOverlayer = false) {
requestAnimationFrame(() => {
const startTime = performance.now();
// 第一帧:核心绘制
this.header.update();
this.footer.update();
this.body.update();
this.ctx.paint.clear();
this.body.draw();
this.footer.draw();
this.header.draw();
this.scroller.draw();
if (!ignoreOverlayer) {
this.overlayer.draw();
}
// 第二帧:自动高度计算,避免影响首帧性能
requestAnimationFrame(() => {
this.body.updateAutoHeight();
});
// 性能监控
const endTime = performance.now();
const drawTime = Math.round(endTime - startTime);
this.ctx.drawTime = drawTime * this.ctx.config.DRAW_TIME_MULTIPLIER;
});
}
2. 虚拟滚动与可见性裁剪
行级虚拟化
Body.ts 中的 update 方法使用二分查找算法快速定位可视区域内的行:
// 基于滚动位置计算可见行范围
const startRowIndex = this.getStartRowIndex();
const endRowIndex = this.getEndRowIndex();
this.renderRows = this.data.slice(startRowIndex, endRowIndex + 1);
列级可见性判断
Header.ts的 update 方法过滤出可见的列:
// 筛选可见的表头单元格
this.renderCenterCellHeaders = this.centerCellHeaders.filter(item =>
item.isHorizontalVisible() && item.isVerticalVisible()
);
this.renderFixedCellHeaders = this.fixedCellHeaders.filter(item =>
item.isHorizontalVisible() && item.isVerticalVisible()
);
3. 分层绘制与遮盖控制
采用分层绘制策略最小化过度绘制:
// Body.ts - 分层绘制顺序
draw() {
this.renderRows.forEach(row => {
// 1. 绘制非固定单元格容器
row.drawContainer();
// 2. 绘制非固定单元格内容
row.drawCenter();
// 3. 绘制固定单元格容器
row.drawFixedContainer();
// 4. 绘制固定单元格内容
row.drawFixed();
});
// 5. 绘制固定列阴影
this.drawFixedShadow();
// 6. 绘制提示线
this.drawTipLine();
}
4. 文本测量与缓存优化
文本分行缓存
Paint类维护了文本缓存映射:
export class Paint {
private textCacheMap = new Map<string, string[]>();
private wrapText(text: string, maxWidth: number, cacheTextKey = ''): string[] {
// 缓存命中检查
if (cacheTextKey && this.textCacheMap.has(cacheTextKey)) {
return this.textCacheMap.get(cacheTextKey) || [''];
}
// 文本分行计算
const lines = this.calculateTextLines(text, maxWidth);
// 缓存结果
if (cacheTextKey) {
this.textCacheMap.set(cacheTextKey, lines);
}
return lines;
}
}
5. 高DPI与像素对齐
Canvas缩放处理
Body.ts中的初始化方法处理高DPI显示:
init() {
const dpr = window.devicePixelRatio || 1;
const canvasWidth = this.ctx.stageWidth * dpr;
const canvasHeight = this.ctx.stageHeight * dpr;
// 设置Canvas实际尺寸
canvasElement.width = Math.round(canvasWidth);
canvasElement.height = Math.round(canvasHeight);
// 设置CSS显示尺寸
const cssWidth = Math.round((canvasElement.width / dpr) * 10000) / 10000;
const cssHeight = Math.round((canvasElement.height / dpr) * 10000) / 10000;
this.ctx.canvasElement.setAttribute('style', `height:${cssHeight}px;width:${cssWidth}px;`);
// 缩放绘制上下文
this.ctx.paint.scale(dpr);
}
像素对齐优化
Paint.ts中的绘制方法使用 -0.5 位移对齐像素网格:
drawRect(x: number, y: number, width: number, height: number, options: RectOptions) {
// -0.5 解决1px边框模糊问题
this.ctx.rect(x - 0.5, y - 0.5, width, height);
}
drawLine(points: number[], options: LineOptions) {
this.ctx.moveTo(points[0] - 0.5, points[1] - 0.5);
for (let i = 2; i < points.length; i += 2) {
this.ctx.lineTo(points[i] - 0.5, points[i + 1] - 0.5);
}
}
6. 覆盖层与DOM最小化
采用Canvas主绘制 + DOM覆盖层的混合架构:
// 主要视觉内容在Canvas上绘制
this.body.draw();
this.footer.draw();
this.header.draw();
// 覆盖层DOM仅在需要时渲染
if (!ignoreOverlayer) {
this.overlayer.draw(); // 限频更新
}
draw(true) 路径跳过覆盖层绘制,降低交互时的卡顿风险。
7. 数据层映射缓存
Database类维护了多种映射缓存:
export default class Database {
private rowKeyMap = new Map<string, any>(); // 行键映射
private colIndexKeyMap = new Map<number, string>(); // 列索引映射
private headerMap = new Map<string, CellHeader>(); // 表头映射
private rowIndexRowKeyMap = new Map<number, string>(); // 行索引到行键
private rowKeyRowIndexMap = new Map<string, number>(); // 行键到行索引
private selectionMap = new Map<string, SelectionMap>(); // 选择状态
private expandMap = new Map<string, boolean>(); // 展开状态
private validationErrorMap = new Map<string, ValidateResult>(); // 验证错误
private positions: Position[] = []; // 虚拟滚动位置
}
这些缓存大幅提高了数据查询效率,避免了重复计算。
8. 图标预处理与缓存
Icons类在首次启动时将SVG转换为 HTMLImageElement 并缓存:
createImageFromSVG(svgContent: string, color: string): HTMLImageElement {
const cacheKey = `${svgContent}_${color}`;
if (this.icons.has(cacheKey)) {
return this.icons.get(cacheKey)!;
}
// SVG转换为Image对象
const img = new Image();
const svgBlob = new Blob([coloredSvg], { type: 'image/svg+xml' });
const url = URL.createObjectURL(svgBlob);
img.src = url;
// 缓存结果
this.icons.set(cacheKey, img);
return img;
}
绘制时直接使用 drawImage 方法,避免重复解析SVG。
性能监控与自适应调优
动态节流调整
根据实际绘制耗时动态调整节流延迟:
const drawTime = Math.round(endTime - startTime);
this.ctx.drawTime = drawTime * this.ctx.config.DRAW_TIME_MULTIPLIER;
throttle函数支持动态延迟计算:
export function throttle(func: Function, delayFunc: () => number) {
let lastCalledTime = 0;
let timeoutId: number | null = null;
return function(...args: any[]) {
const now = Date.now();
const delay = delayFunc(); // 动态计算延迟
if (now - lastCalledTime >= delay) {
func.apply(this, args);
lastCalledTime = now;
} else if (!timeoutId) {
timeoutId = window.setTimeout(() => {
func.apply(this, args);
lastCalledTime = Date.now();
timeoutId = null;
}, delay - (now - lastCalledTime));
}
};
}
这个Canvas高性能表格架构通过以下核心优化实现了卓越的性能:
- 虚拟滚动:只渲染可见区域,支持海量数据
- 分层绘制:最小化过度绘制,提高渲染效率
- 智能缓存:文本、图标、数据映射多层缓存
- 帧调度:自适应节流控制,保证交互流畅性
- 高DPI优化:像素对齐,保证清晰度
- 混合架构:Canvas绘制 + DOM覆盖层,兼顾性能与功能