一张 DataGrid 如何工作:从流畅渲染到连续录入

做后台系统,基本绕不开表格。

订单、库存、财务流水、项目台账、客户资料和操作日志,最后大多要用表格展示。只看最简单的例子,DataGrid 好像就是接收一组列和一个数组,然后画出表头、行和滚动条。

业务页面当然没有这么简单。固定列、排序、筛选、分组、汇总、复制、键盘操作、单元格编辑、输入校验、Lookup 和大数据滚动经常出现在同一张表里。功能叠加以后,既要保证状态一致,也要控制滚动和输入时的开销。

这里主要关心两件事。数据量增加后,首帧呈现、滚动和交互不能明显变慢;用户开始录入后,键盘导航、单元格编辑、Popup 选择和校验提交也不能互相打断。

ds-ui 的 DataGrid 同时处理这两类问题。绘制部分面向较大的数据集,编辑部分则参考了 DevExpress WinForms GridControl 等成熟桌面端 DataGrid 的使用方式,尤其是连续录入、键盘导航和单元格编辑。

上一篇文章讲了从 WinForms 到 Vue:我为什么决定在 Web 上重做一套完整的 GUI 框架。这篇先只看 ds-ui 的 DataGrid:数据怎样进入视口,一次滚动怎样变成下一帧画面,鼠标怎样找到单元格,以及编辑会话怎样稳定地接管输入、焦点、Popup 和校验。

文末使用十万行数据做了一次压力测试,目的是放大性能问题,观察每帧工作量。实际业务仍然要根据数据来源和查询方式选择分页或服务端查询。

事件、绘制和图层合成会顺带提到,GUI Runtime 留到下一篇再展开。

项目官网:hleditor.com

DataGrid 在线演示:打开 Live Example

这个 Showcase 把固定列、筛选、范围选择、编辑、Lookup、分组汇总和大数据压力场景放在了同一张表里。


一、业务代码只关心列、数据和规则

先看一个普通的业务数据模型:

ts 复制代码
interface BusinessRecord {
	id: number;
	name: string;
	category: string;
	status: "active" | "review" | "paused";
	owner: string;
	department: string;
	amount: number;
	progress: number;
	updatedAt: string;
}

创建 DataGrid 时,业务代码只需要说明数据、列和需要开启的功能:

ts 复制代码
const grid = new RenderDataGrid<BusinessRecord>({
	rowKey: "id",
	columns,
	rows,
	selectionMode: "range",
	editable: true,
	sortable: true,
	resizableColumns: true,
	reorderableColumns: true,
	onCellChange: (row, key, value) => {
		syncBusinessState(row.id, key, value);
	},
});

DataGrid 会先完成有效值的写回,再触发 onCellChange,因此这个回调更适合用来同步外部状态、记录待保存变更或驱动字段联动。后续筛选、选择、新增和编辑,都可以通过 setFilters()selectRange()addRows()beginEdit() 等 API 完成。状态颜色和编辑权限则交给 resolveCellStyle()resolveCellEditPolicy()

列配置还描述了操作规则:哪些单元格可编辑、哪些字段只读或禁用、Tab 应该停在哪里、输入何时有效,以及 Lookup 应该显示哪些候选项。DataGrid 在编辑和键盘导航时读取这些规则。

至于下面这些事情,应该由 DataGrid 自己处理,而不是每个页面重新写一遍:

  • 当前单元格怎样进入编辑,提交后怎样继续移动到下一个业务字段;
  • editablereadonlydisabledtabStop 怎样共同决定录入路径;
  • 文本、数字、Lookup、日期、树和树表编辑器怎样遵守同一套提交规则;
  • 排序、筛选和分组以后,当前记录究竟位于第几行;
  • 滚动到任意位置时,本帧应该处理哪些单元格;
  • 固定列和普通列怎样共享同一个纵向视口;
  • 鼠标坐标对应哪一行、哪一列;
  • 键盘焦点、当前单元格和选择范围怎样协同;
  • 一次 hover、滚动或输入,是需要重新布局,还是只重画一帧。

二、一张表格,内部有几套分工

业务代码只会接触 RenderDataGrid,但组件内部并不是一个类包办所有事情。数据处理、视口、选择、编辑和绘制各有自己的模块:

对外只有一个 DataGrid,对内则按数据、视口、交互、编辑和绘制分工。

拆开以后,各部分的边界更清楚。数据投影、键盘导航可以分别测试,绘制模块也能单独做性能优化。

DataGrid 内部实际上同时运行着两条链路:

  • 渲染链路:源数据 → 数据投影 → 可视范围 → 单元格绘制;
  • 编辑链路:当前单元格 → 编辑会话 → 输入桥或 Popup → 校验 → 提交 → 下一个 Tab 停靠点。

两条链路共享行身份、单元格几何、焦点状态和组件生命周期。从滚动切换到输入时,不需要在两套 UI 模型之间交接状态。

比如滚动时,视口模块(Viewport)只更新偏移量,绘制模块(Painter)根据新的可视范围重画,交互模块(Interaction)继续保存当前选择。如果正在编辑,编辑模块还要检查编辑器是否仍在屏幕里。一次滚轮事件不需要把这些逻辑全塞进同一个函数。


三、数组里的第 20 行,不一定还是屏幕上的第 20 行

表格内部至少要区分三种位置:

  1. 源数据索引:记录在原始 rows 数组中的位置。
  2. 可视数据索引:排序、筛选后的记录位置。
  3. 可视项目索引:加入分组标题后的屏幕项目位置,分组标题本身不是业务记录。

排序、筛选等规则变化时更新投影;普通滚动只换一段可视范围。

ds-ui 不会为了当前视图复制一遍所有业务对象。它保存的是原始记录和当前投影之间的索引关系,并缓存排序、筛选、分组和汇总结果。只有数据或规则改变时,相关缓存才会重算。

所以选择和编辑不能只记住"第 20 行"。一旦用户重新排序,这条记录可能已经跑到别的位置。要想继续找到它,必须使用业务对象或稳定的 rowKey

虚拟化只减少屏幕附近的布局和绘制,不能让全量排序、筛选消失。十万条记录仍然在内存里,客户端排序也仍然要处理十万条。数据再大一些,还是要考虑分页、服务端查询和输入防抖。


四、没有单元格 DOM,改变的不只是滚动

现在很多 Web 表格都有虚拟滚动,但"都只渲染可视区域"不代表它们的实现方式相同。

下面比较的是常见的 DOM 虚拟列表或 DOM 虚拟表格。Canvas 和 WebGL 当然也属于 Web 技术,这里只是为了方便区分两种实现。

成熟桌面 DataGrid 的共同思路,是由 Grid 统一管理当前单元格、键盘导航、in-place editor 与提交生命周期,而不是给每个单元格长期放置一个输入控件。ds-ui 在浏览器中采用了相近的控件思路:当前单元格首先由逻辑行列身份、状态与几何定义,而不是由一个单元格 DOM 节点定义。

典型 DOM 方案以限制可视节点为主要边界;ds-ui 则直接从索引、几何、命中和绘制阶段控制本帧工作。

DOM 虚拟表格:保留少量行节点

普通 DOM 表格直接渲染十万行,会产生大量行节点和单元格节点,创建、样式计算、布局和内存压力都会上升。

典型虚拟滚动会使用占位层、位移层或分段滚动空间表达逻辑总高度,同时只挂载视口附近的一批行。滚动时,框架创建、复用或移动这些 DOM 节点。

这样可以把 DOM 数量从与总数据量相关,压缩到主要与视口和 overscan 相关。可视行仍然是真实节点,所以更容易建立在浏览器的文本布局、CSS、原生滚动和可访问性基础设施之上。

ds-ui DataGrid:没有行节点

ds-ui 的 Canvas DataGrid 不会给每一行创建 DOM。不可见行没有节点,可见行也没有节点,屏幕上的单元格只是这一帧的绘制结果。

完整内容高度只是一个数值:

text 复制代码
totalHeight = visibleItemCount × rowHeight

当前滚动位置也是 DataGrid 自己维护的视口状态。固定行高下,可视范围可以直接通过索引计算得到:

text 复制代码
visibleStart = floor(scrollY / rowHeight)
visibleEnd   = ceil((scrollY + viewportHeight) / rowHeight) - 1

组件会在可视范围前后多留几行缓冲,然后只计算和绘制这一小段内容。行在屏幕上的位置也能直接算出来:

text 复制代码
rowY = bodyY + rowIndex × rowHeight - scrollY

在行和单元格层面,没有旧 DOM 节点需要搬动。DataGrid 只是在下一帧换一段索引,再把新内容画到既有的 Canvas 图层中。

鼠标命中也靠计算:先用鼠标坐标、滚动偏移和行高算出行,再根据列宽算出列,不需要去 DOM 树里找单元格元素。

overscan 不是两者的主要区别

两种方案都可以使用固定行高、可视范围和 overscan,也都能把滚动工作限制在视口附近。典型 DOM 虚拟化以限制已挂载节点为主要手段,并由此缩小组件渲染、样式、布局与绘制范围;ds-ui 不创建行和单元格节点,直接控制本帧的数据解析、索引计算、命中测试和绘制工作。

对比项 典型 DOM 虚拟表格 ds-ui Canvas DataGrid
主要 UI 载体 视口内的行、单元格 DOM 与组件实例 既有 Canvas 图层中的绘制结果
可视范围 主要限制已挂载节点及相关组件工作 直接限制索引、数据解析、命中与绘制指令
总高度表达 内部数值状态加浏览器滚动空间或占位层 Runtime 数值状态加自绘滚动条
行定位 几何计算后写入节点位置或 transform 几何计算后直接生成绘制坐标
单元格识别 可利用事件目标或委托,也可结合坐标几何 统一由坐标映射到行列索引
编辑核心 活动编辑组件与逻辑编辑状态协同 逻辑编辑会话与共享输入、Popup 服务协同
键盘焦点 Grid 状态协调单元格和编辑器的 DOM 焦点 Runtime 协调逻辑焦点与共享输入桥
Popup 编辑 Grid 或 Popup 服务协调 Portal、锚点、外部点击和滚动 Popup 服务直接复用 Grid 几何和生命周期
浏览器工作 可视节点仍参与样式、布局和绘制 Canvas 元素参与页面布局,内部单元格由框架绘制
原生语义 更容易利用文本、CSS 与可访问性基础设施 需要框架额外承接输入、复制和可访问性

ds-ui 在行和单元格层面没有 DOM 节点需要回收。本帧要处理哪些数据、命中哪些单元格、生成多少绘制指令,都由索引范围直接决定。

进入编辑后,典型 DOM DataGrid 还要协调虚拟节点、活动编辑组件、浏览器焦点、Popup Portal 和滚动位置。ds-ui 的浏览、命中和编辑不依赖单元格 DOM 的生命周期,相关状态都由 Runtime 管理。

AG GridMUI X 都能在 DOM 架构下提供成熟的单元格编辑、键盘导航和自定义编辑能力。它们通过编辑组件、焦点规则和 Portal 协议处理这些关系;ds-ui 使用自己的 Runtime、逻辑焦点和单元格几何处理。

Canvas 并不会自动带来更高性能。JavaScript、文本测量、Canvas 绘制和浏览器最终显示都要花时间;如果每一帧都重新排序或创建大量临时对象,照样会卡。

DOM 表格也有很明显的优势。数据量不大、行高不固定、单元格里有复杂富文本,或者项目很重视原生可访问性时,成熟的 DOM 表格通常更省事。选哪一种,应该看页面需求,而不是只比一句"谁更快"。


五、一次滚动如何变成屏幕上的下一帧

DataGrid 虽然自己管理滚动,但鼠标输入、事件分发和帧调度仍然由 GUI Runtime 统一处理。

一次滚轮或触控板操作,大致会经过下面几步:

有输入并且状态发生变化时,运行时才安排新一帧。

在 60 Hz 屏幕上,一帧预算约为 16.7 ms;到了 120 Hz,预算则只有约 8.3 ms。刷新率并不要求组件树持续重画。页面没有变化时不安排新帧;滚动、hover、输入或动画触发的多次绘制请求,也会合并到同一帧。

滚动通常只修改视口偏移。表格尺寸、列宽、行高或父布局约束发生变化时,才重新计算布局。

本文里的 composite,指 ds-ui 把主内容、Popup、Window、Overlay 等图层合到最终 Canvas 的过程。它不等于浏览器开发者工具里完整的 GPU 合成阶段。

事件捕获、焦点路由、脏布局传播和输入法桥接,留到下一篇 GUI Runtime 再讲。


六、只绘制可视行还不够

可视范围是大表格性能的基础,但不是全部。

1. 固定行高让计算更简单

当前 DataGrid 使用统一的 rowHeight。代价是不适合大量动态行高内容,好处是坐标关系简单而稳定。

只要知道 scrollYrowHeightviewportHeight,就能直接算出可视行范围,也能把鼠标坐标换算成行索引。程序跳转到某条记录时,同样不需要逐行测量。

2. 显示回调只处理可视单元格

金额格式、状态颜色、错误边框和只读样式,最后都要转换成单元格的显示结果。

getCellDisplayText()resolveRowStyle()resolveCellStyle()resolveCellEditPolicy() 只对当前可视区及少量 overscan 单元格执行。业务回调要尽量轻量,不要在里面请求网络、遍历大对象或重建整份数据。

这些回调会在滚动绘制时频繁调用。一次数组查找看起来很快,但乘上可视行数、列数和帧数后,成本就可能很明显。

3. 固定列和普通列共用同一段可视行

左固定列、普通列和右固定列使用不同的横向区域,但共享同一段可视行。固定列不会导致全量行绘制,只是同一行要在几个裁剪区域里分别画。

横向滚动时,完全看不见的普通列不会参与单元格解析和绘制。实际工作量仍然跟当前视口有关,而不是"总行数 × 总列数"。

4. 排序、过滤、分组和 Lookup 都需要缓存

虚拟化解决的是绘制范围,数据模型还要避免每次 paint 都重新排序、过滤和分组。

DataGrid 会缓存当前投影。数据或规则改变时才重算;普通 hover 和滚动不会重新处理十万条数据。

Lookup 列也会建立值到候选记录的索引,避免显示、复制和筛选时反复扫描整个候选数组。

5. 自动列宽不能扫描全部数据

如果为了计算"最合适"的列宽而测量十万行文本,虚拟化节省下来的成本很快会被抵消。

大数据表格最好给出明确列宽。需要自动列宽时,应限制采样数量,并避免在滚动、输入或 hover 时反复测量。长文本可以用省略号和 Tooltip,不必为了少数内容无限拉宽列。

6. 诊断不能只看平均 FPS

平均 FPS 很容易藏住偶发卡顿。一百帧里即使九十九帧都很快,只要有一帧卡住一百毫秒,用户就能感觉到。

更有价值的指标包括:

  • 本帧实际绘制了多少行、多少单元格;
  • 可视范围和 overscan 范围是多少;
  • 当前 scrollY 和总内容高度是多少;
  • 本帧是否执行了 layout;
  • layout、主内容 paint、临时图层 paint 和 composite 分别用了多久;
  • 排序、筛选或分组缓存为什么失效;
  • 长时间滚动和反复编辑后,编辑会话是否正确结束,临时对象是否被释放。

这些数据比一句"看起来很流畅"更能说明问题出在哪里。


七、流畅不只发生在滚动时

高频录入会暴露浏览状态下看不到的问题:编辑器切换是否跳动,Tab 会不会中断,Popup 滚动后是否还对得上单元格,校验失败后能不能留在原处继续修改。

ds-ui 参考了 DevExpress WinForms GridControl 等桌面 DataGrid 的操作方式,由 Grid 管理当前单元格、编辑会话、焦点路径、编辑器和提交规则。成熟 Web DataGrid 也能采用这种组织方式。ds-ui 的具体选择,是让这些功能与绘制、Popup、输入桥和 GUI Runtime 共用状态与几何。

1. 编辑会话不依附于单元格 DOM

普通单元格只保存数据和显示状态,不会各自持有一个输入控件或编辑器对象。每个 DataGrid 持有统一的编辑控制器,并按类别维护可复用的 Popup 宿主。

用户双击单元格或按下编辑快捷键后,文本、数字和 Popup 类编辑器会建立逻辑编辑会话,关联当前行列身份、原始值和草稿;单元格几何与校验规则在提交时按需解析。复选框没有持续会话,切换后直接进入提交管线。编辑结束时清理临时状态,编辑控制器和 Popup 宿主留给下一个单元格使用。Lookup 等复杂 Popup 的内部子视图仍会按打开会话创建和释放。

很多基于 DOM 或组件框架的表格,会在单元格进入编辑状态时挂载活动编辑组件,退出时再卸载。成熟实现会保存编辑状态、复用实例,并处理虚拟化后的重建。ds-ui 没有这层单元格 DOM 生命周期,编辑会话直接由 Runtime 持有。

共享编辑器并非 Canvas 特有。以 Windows Forms DataGridView 为例,微软官方文档说明它一次只承载一个 editing control,并在连续编辑同类型单元格时复用。ds-ui 也使用共享编辑模型,并把它接入 Canvas 几何、逻辑焦点、输入桥和 Popup Runtime。

2. textarea 只负责输入,编辑外观仍由 DataGrid 绘制

DataGrid 没有在 Canvas 上覆盖一个带样式的可见 DOM 输入框。Runtime 共享一个透明 textarea,用它接收浏览器键盘、剪贴板和中文 IME。为保证输入法候选框位置正确,textarea 会跟随自绘光标更新坐标,但不参与视觉呈现。

编辑文本或数字时,单元格背景、边框、文字、选区、闪烁光标、IME 预编辑文本、数字步进按钮和校验状态都由 ds-ui 绘制。下拉框、Lookup、日期、树和树表选择器使用相同的组件绘制和 Popup 管理机制,进入编辑时不会切换到另一套 DOM 样式和盒模型。

浏览态和编辑态使用同一份单元格几何。滚动、固定列或列宽变化时,文字、光标、命中区域和 Popup 锚点一起更新,不需要再同步一个可见 DOM 输入框的位置。

3. 连续键盘录入由 Grid 统一管理

高频录入很依赖键盘路径。ds-ui 目前支持:

  • EnterF2 进入当前单元格编辑,复选框可以用 Space 切换;
  • 编辑中按 Enter 提交,按 Escape 放弃草稿;
  • Tab / Shift+Tab 在校验和提交后移动到下一个或上一个有效停靠点;
  • readonly 单元格可以按业务规则参与 Tab 路径但不能修改,disabled 单元格默认跳过;
  • 数字编辑器可以用 ArrowUp / ArrowDown 按精度和边界步进;
  • Popup 打开后,方向键、确认和取消优先交给当前 Popup。

文本、数字和 Popup 类编辑器有持续会话及提交、取消过程,复选框则即时切换。它们都经过相同的权限、值标准化、校验和写回流程:

  • text:单行文本编辑;
  • number:带最小值、最大值、步长和小数精度的数字编辑;
  • checkbox:布尔值切换;
  • select:固定值域的下拉选择;
  • lookup:可搜索、多字段展示的表格查询选择;
  • date:日期选择;
  • drop-tree:树形数据选择;
  • drop-tree-grid:带层级和多列信息的树表选择。

Lookup 查询还能读取当前外层单元格和业务行。同一列可以根据当前记录过滤候选项,而不需要为每一行创建不同的编辑器。

当前编辑单元格的文字、光标、选区和边框同样由 Canvas 绘制,textarea 只负责接收文本输入。

固定值域直接使用简单的单选列表。

Lookup 可以同时显示姓名、团队和人员编码,并支持搜索。

日期选择器贴着当前单元格打开,确认后再写回数据。

4. 草稿、校验和提交

文本和数字输入先写入编辑草稿,不会每敲一个字符就修改业务数据。用户按下 EnterTab、点击其他单元格或者显式调用 commitEdit() 时,DataGrid 执行值标准化、单元格校验和提交。

校验失败时,当前编辑会话和草稿会被保留,焦点不会跳到下一个单元格,也不会触发 onCellChange;错误边框和原因提示由 DataGrid 绘制。校验通过后,值才写回当前记录。需要提交整张业务表单时,还可以显式调用 validate() 检查全部源数据行并执行跨字段的行级校验;校验不通过时,再调用 focusFirstError() 定位第一个可见错误。

resolveCellEditPolicy() 决定每个单元格是 editablereadonly 还是 disabled,是否参加 Tab 路径,以及为什么不可编辑。鼠标、键盘、复选框、Popup、粘贴和最终提交都会读取这份策略。

5. 编辑会话跟随行身份和单元格几何

一个很常见的业务动作是新增记录后直接编辑第一项:

ts 复制代码
grid.addRows([newRecord]);
grid.beginEdit(newRecord, "itemName");

这时不能只是把 editing 改成 true

新记录可能在当前纵向视口之外,也可能因为排序出现在别的位置。DataGrid 要先在当前数据投影中找到它,再把目标行纵向滚入视口,最后才能定位编辑器:

新增行不在屏幕里时,先把目标行纵向滚入视口,再让对应编辑器接管该单元格。

如果记录已经可见,nearest 不会多滚一下;如果记录被筛选条件隐藏,beginEdit() 会返回失败,而不是偷偷清掉用户的筛选条件。当前 API 只保证目标行纵向可见,不会主动改变横向滚动位置。

排序、筛选或行投影变化后,只要目标记录仍在当前投影中,编辑会话就会根据行对象或稳定的 rowKey 重新定位,而不是继续依赖旧的可视行号;目标记录被筛选排除时,这次编辑会话会结束。滚动和布局变化时,文本编辑位置与 Popup 锚点使用同一份单元格几何更新;Popup 目标离开视口后,DataGrid 会按明确的生命周期结束这次 Popup 编辑,而不是留下一个已经失去目标的浮层。

编辑结束后,输入状态和 Popup 会话都要正确结束,临时子视图、事件监听和定时器也要及时释放;可复用的控制器和 Popup 宿主则继续保留。反复编辑几千次后,如果临时对象越积越多,表格一样会慢下来。

给单元格设置 editable:true 只打开了编辑入口。定位、输入、校验、提交、Popup 和下一个 Tab 停靠点都要接得上,用户才能沿着表格一直操作下去。


八、压力验证:把数据放大到十万行之后

下面把数据放大到 100,000 行做压力测试。这个规模不代表常规项目的数据交付方式,主要用来观察滚动工作量、全量数据操作,以及编辑会话接管当前单元格时的启动开销。

测试数据来自本文 Showcase 的生产构建。测试机器是 M1 Pro / 16 GB,浏览器为 Chromium 150,DPR 为 2,Canvas 尺寸是 1280 × 720。表格包含 100,000 行、10 列。

每轮先滚动 12 帧预热,再采集 100 个滚动帧;连续测试 3 轮,共记录 300 帧。测试时关闭可视性能浮层,只保留后台计时,避免图表本身参与绘制和合成。触发测试动作后,探测器会先等待并记录被测帧,再读取诊断状态,避免提前计算表格汇总并预热缓存。

表中的 P50 可以理解为中位数,P95 和 P99 用来观察较慢的那部分帧。

指标 结果
工厂创建至首帧可见 P50 48.5 ms
首帧总耗时 P50 19.0 ms
滚动整帧 P50 2.8 ms / P95 3.2 ms / P99 3.9 ms
主内容绘制 P50 2.4 ms / P95 2.7 ms
10 万行排序 P50 18.8 ms
开始编辑到画面稳定 P50 7.9 ms
全表快速筛选 P50 833.8 ms

首帧和滚动指标观察视口绘制,排序和快速筛选观察全量数据操作。"开始编辑到画面稳定"只测量一次编辑会话的启动成本;7.9 ms 不代表完整的连续录入过程。Tab 提交、Popup 打开和长时间反复编辑还需要单独采样。

首帧总耗时是一次性的初始化样本,不代表稳定滚动帧。持续交互能否落入帧预算,应主要观察滚动整帧的 P95 和 P99。

视口里大约能看到 15 行,加上边界情况和前后缓冲,本次采样每帧实际处理约 16~18 行,也就是约 160~180 个单元格。滚动一帧大约 2.8 ms,说明滚动时的工作量已经控制在视口附近。

快速筛选则不同。它要检查十万条记录,所以仍然需要约 0.83 秒。虚拟滚动帮不了这部分工作。实际项目中应加输入防抖、缩小查询字段,数据再大时改用服务端查询。

正式数据只包含隐藏浮层时的后台采样,不把性能图表本身的绘制成本算进 DataGrid 帧耗时。浮层用于开发阶段观察趋势,不能和业务内容一起参与正式基准。

性能浮层可以在开发时查看整帧、布局、主内容绘制和合成耗时。上面的正式数据使用后台采样,测试时没有显示这个浮层。

这组结果用来观察框架的工作量边界。实际页面仍应根据查询模式选择分页、服务端筛选和输入防抖。


九、哪些页面适合使用它

ds-ui DataGrid 自己管理视口、命中测试、绘制、焦点和编辑器。这样做控制力更强,但也意味着框架要承担更多工作。

比较适合的场景有:

  • 高信息密度、长时间驻留、接近 DevExpress WinForms GridControl 等成熟桌面 DataGrid 操作方式的业务工作台;
  • 医嘱、订单、费用明细、物料、字典和配置项等需要连续 Tab 录入、复杂 Lookup 与严格校验的页面;
  • 大量固定结构数据的连续浏览;
  • 强键盘导航、范围选择和复制;
  • 需要单元格编辑、校验和 Popup 选择器协同的页面;
  • 希望表格的输入、弹层和其他 GUI 组件保持一致的项目。

它也有明确的使用边界:

  • 当前所有数据行共用统一的 rowHeight,不支持按行动态高度、合并单元格或跨行布局。需要自适应多行排版时,应选择更合适的表格或文档组件;
  • 单元格适合结构化文本、状态和字段编辑,不以承载任意 DOM 结构或复杂富文本为目标;
  • 当前编辑体系包含八类内置单元格编辑器和同步校验,暂不支持撤销重做、整行编辑事务、任意自定义编辑器和 Excel 式矩形粘贴;
  • 键盘输入、复制、粘贴和焦点已经由框架接管,但 Canvas 不能直接继承逐单元格的 DOM 语义。项目有严格的屏幕阅读器要求时,还需要单独评估可访问性适配;
  • 内置排序、筛选、分组和汇总在客户端执行。数据量很大或来自远程接口时,应由业务层分页、查询并向 DataGrid 提供当前结果集;
  • 少量静态数据当然也可以使用 DataGrid,只是虚拟滚动、复杂选择和编辑体系的价值不明显,此时轻量表格通常更省事。

文中以 DevExpress WinForms GridControl 为参照,参照的是桌面 DataGrid 的交互原则,并不表示两者的功能已经对等。浏览、定位、输入、校验、选择器和键盘导航由一个控件统一协调,ds-ui 用 Canvas GUI Runtime 实现了类似的组织方式。

数据投影、视口、焦点、Popup 和校验都放在 DataGrid 内部管理。数据量变大时控制每帧绘制范围,进入编辑后保持操作路径连续,这是当前实现的两个重点。