18 KiB
18 KiB
事件处理机制
**本文引用的文件** - [FmsTable.vue](file://fms-vue/src/components/fms-table/FmsTable.vue) - [createCellRenderer.js](file://fms-vue/src/components/fms-table/utils/createCellRenderer.js) - [createEditableCellRenderer.js](file://fms-vue/src/components/fms-table/utils/createEditableCellRenderer.js) - [createSelectionCellRenderers.js](file://fms-vue/src/components/fms-table/utils/createSelectionCellRenderers.js)目录
简介
本技术文档聚焦 FMS 表格的事件处理机制,围绕以下目标展开:
- 解释表格事件类型与处理逻辑:area-selection-change(选区变化)、row-order-change(行拖拽排序)、change(数据变更)等。
- 深入说明键盘导航事件处理:NAVIGATION_KEYS 集合、handleKeydownCapture 捕获处理、handleKeyupCapture 释放处理。
- 阐述事件冒泡与捕获机制:@keydown.capture 的使用与事件优先级控制。
- 描述编辑相关事件流:编辑器激活、值变更确认与取消操作的完整流程。
- 提供最佳实践:事件委托、性能优化与错误处理方案。
项目结构
FMS 表格位于 fms-vue 的 fms-table 组件中,核心由一个容器组件与若干工具模块组成:
- 容器组件:FmsTable.vue,负责事件绑定、状态管理、与底层 StkTable 的交互。
- 工具模块:
- createCellRenderer.js:解析列渲染策略,决定是否注入可编辑/动作单元格。
- createEditableCellRenderer.js:实现输入框、选择器、复选框等编辑器的渲染与事件。
- createSelectionCellRenderers.js:生成选择列(含全选/单选)及内部状态管理。
graph TB
A["FmsTable.vue"] --> B["StkTable(底层表格库)"]
A --> C["createCellRenderer.js"]
C --> D["createEditableCellRenderer.js"]
A --> E["createSelectionCellRenderers.js"]
A --> F["窗口级键盘监听<br/>handleKeydownCapture / handleKeyupCapture"]
图表来源
- FmsTable.vue:557-584
- createCellRenderer.js:208-228
- createEditableCellRenderer.js:195-211
- createSelectionCellRenderers.js:18-129
章节来源
核心组件
- FmsTable.vue:对外暴露 props、emits 与方法;集中处理 area-selection-change、row-order-change、change 等事件;维护编辑态与键盘导航状态;通过 @keydown.capture 在捕获阶段拦截方向键/Tab,协调编辑器激活时机。
- createCellRenderer.js:根据 editingActive 与列配置决定注入 customCell(可编辑或 actions),否则走原生 td 路径以减少渲染开销。
- createEditableCellRenderer.js:实现 Input/Select/Checkbox 编辑器,处理焦点、草稿提交、值变更通知与取消(Escape)。
- createSelectionCellRenderers.js:生成选择列,封装选中状态计算与 onChange 回调。
章节来源
- FmsTable.vue:10-39
- createCellRenderer.js:208-228
- createEditableCellRenderer.js:195-211
- createSelectionCellRenderers.js:18-129
架构总览
下图展示了表格事件从 DOM 到业务层的流转路径,包括捕获阶段的键盘导航、选区变化、行拖拽排序以及编辑变更通知。
sequenceDiagram
participant U as "用户"
participant T as "FmsTable.vue"
participant S as "StkTable"
participant R as "渲染器(createCellRenderer)"
participant E as "编辑器(createEditableCellRenderer)"
participant P as "父组件"
U->>T : "@keydown.capture 方向键/Tab"
T->>T : "handleKeydownCapture() 记录按键/重复标志"
T->>S : "area-selection-change(ranges)"
S-->>T : "ranges(选区信息)"
T->>T : "handleAreaSelectionChange() 决策是否激活编辑"
alt 需要编辑
T->>R : "resolveCustomCell(col, editingCtx, editingActive)"
R->>E : "createEditableCell(...) 返回编辑器"
E->>P : "onChange(row,col,value) via ctx.onChange"
else 非编辑
T->>S : "保持原生 td 渲染"
end
U->>S : "拖拽行"
S-->>T : "row-order-change(sourceIndex,targetIndex)"
T->>P : "emit('row-order-change', rows, sourceIndex, targetIndex)"
图表来源
- FmsTable.vue:413-448
- FmsTable.vue:451-505
- FmsTable.vue:310-314
- createCellRenderer.js:208-228
- createEditableCellRenderer.js:66-70
详细组件分析
键盘导航事件处理(捕获与释放)
- 捕获阶段拦截:在根元素上使用 @keydown.capture 将方向键/Tab 的捕获阶段交给 handleKeydownCapture,确保在子组件(如输入框)之前优先处理导航逻辑。
- 导航键集合:NAVIGATION_KEYS = {ArrowUp, ArrowDown, ArrowLeft, ArrowRight, Tab}。
- 按下处理:
- 首次按下时,动态在 window 上注册 keyup 捕获监听与 blur 监听,用于后续释放处理与失焦清理。
- 记录 pressedNavigationKeys 集合以跟踪当前按下的导航键数量。
- 使用 event.repeat 判断是否为按住重复触发,设置 isRepeatNavigation 标志,用于区分“长按滚动”和“离散点按”。
- 释放处理:
- handleKeyupCapture 在对应键释放时从集合移除;当集合为空时移除所有监听并进入最终激活逻辑。
- 若为长按结束,立即 commitPendingEditing,不再等待防抖。
- 若为快速连按,使用 scheduleEditActivation 进行短防抖合并,避免频繁挂载/卸载编辑器导致掉帧。
- 失焦处理:handleWindowBlur 清理监听与状态,确保异常退出编辑态。
flowchart TD
Start(["keydown 捕获"]) --> CheckNav{"是导航键?"}
CheckNav -- 否 --> End(["忽略"])
CheckNav -- 是 --> FirstPress{"首次按下?"}
FirstPress -- 是 --> AddListener["添加 window keyup/blur 捕获监听"]
FirstPress -- 否 --> SkipAdd["跳过添加监听"]
AddListener --> Record["记录按键到 pressedNavigationKeys"]
SkipAdd --> Record
Record --> Repeat{"event.repeat?"}
Repeat -- 是 --> SetFlag["设置 isRepeatNavigation=true"]
Repeat -- 否 --> NoFlag["不设置标志"]
SetFlag --> WaitKeyUp["等待 keyup"]
NoFlag --> WaitKeyUp
WaitKeyUp --> KeyUp["keyup 捕获"]
KeyUp --> RemoveKey["从集合移除该键"]
RemoveKey --> AnyLeft{"集合是否为空?"}
AnyLeft -- 否 --> End
AnyLeft -- 是 --> Cleanup["移除监听/清理状态"]
Cleanup --> LongPress{"长按结束?"}
LongPress -- 是 --> CommitNow["立即 commitPendingEditing"]
LongPress -- 否 --> Debounce["短防抖 scheduleEditActivation"]
Debounce --> CommitNow
CommitNow --> Activate["activateEditing(row,focusCol)"]
Activate --> End
图表来源
章节来源
选区变化事件(area-selection-change)
- 触发时机:用户在表格中进行区域选择(鼠标拖选或键盘扩展选区)时,StkTable 发出 area-selection-change。
- 处理逻辑:
- 获取选区最后一个范围的 end 行列索引,定位目标行与列。
- 若存在 editor 配置的列,则准备激活该行编辑,并将 focusCol 设置为该列 dataIndex。
- 键盘导航期间:
- 按住方向键滚动(repeat=true):全程保持无编辑器,仅缓存 pendingEditingTarget,松手后统一激活,避免反复创建组件导致卡顿。
- 慢速离散点按:立即激活编辑器,提升跟手性。
- 快速连按(间隔 <150ms):使用短防抖合并激活,减少重渲染。
- 非键盘导航(鼠标点击):立即激活;若同一行已编辑,更新 focusCol 并临时禁用 selectOnFocus 以避免文本全选。
- 结果:通过 activateEditing 与 setCurrentRow 使目标行进入编辑态并聚焦对应编辑器。
sequenceDiagram
participant S as "StkTable"
participant T as "FmsTable"
participant R as "渲染器"
participant E as "编辑器"
S-->>T : "area-selection-change(ranges)"
T->>T : "handleAreaSelectionChange()"
T->>T : "定位 row/col"
alt 键盘导航且 repeat
T->>T : "pendingEditingTarget={row,focusCol}"
else 键盘导航且非 repeat
T->>T : "防抖/立即激活"
else 鼠标操作
T->>T : "立即激活"
end
T->>R : "resolveCustomCell(..., editingActive)"
R->>E : "createEditableCell(...)"
E-->>T : "focusCol 聚焦"
图表来源
章节来源
行拖拽排序事件(row-order-change)
- 触发时机:启用 rowDraggable 后,用户拖拽行改变顺序,StkTable 发出 row-order-change。
- 处理逻辑:
- 清除当前选区,避免拖拽后选区残留。
- 获取当前表格数据源(或回退到 props.dataSource)。
- 向父组件 emit('row-order-change', [...rows], sourceIndex, targetIndex),由父组件完成数据重排与持久化。
章节来源
数据变更事件(change)
- 触发时机:编辑器值发生变化并提交时。
- 处理逻辑:
- 编辑器内部通过 notifyChange(ctx, editor, row, col, value) 调用 ctx.shouldNotify 判断是否需要上报(例如 select/checkbox 使用严格相等比较,其他类型使用字符串化比较)。
- 若需上报,调用 editor.onChange 与 ctx.onChange,后者即 emit('change', row, col, newValue)。
- 取消操作:
- 输入框支持 Escape 键触发 blur,从而丢弃草稿并恢复原值。
- 选择器直接更新 row 字段并通知 change。
- 复选框在 mousedown 阶段阻止事件冒泡,避免与表格选区/编辑激活冲突。
sequenceDiagram
participant E as "编辑器"
participant C as "ctx(shouldNotify/onChange)"
participant T as "FmsTable"
participant P as "父组件"
E->>C : "notifyChange(row,col,value)"
C->>C : "shouldNotify(row,col,value)"
alt 需要上报
C->>E : "editor.onChange(value,row,col)"
C->>T : "ctx.onChange(row,col,value)"
T->>P : "emit('change', row, col, newValue)"
else 不需要上报
C-->>E : "忽略"
end
图表来源
章节来源
编辑激活与取消流程
- 激活:
- 通过 activateEditing(row, focusCol) 设置行编辑态,记录初始值 captureEditableValues,并标记 editingActive。
- 渲染器根据 editingActive 决定是否注入 customCell(可编辑列显示编辑器),否则走原生 td。
- setCurrentRow 静默高亮目标行,保证视觉一致。
- 取消:
- 输入框 Escape 触发 blur,丢弃草稿并恢复原值。
- 切换编辑行时 deactivateRowEditing 清理旧行编辑态。
- 离开窗口或禁用 editable 时,清理监听与状态,并清空选区。
章节来源
选择列与全选逻辑
- 选择列由 createSelectionColumn 生成,包含:
- 表头全选:统计当前数据中的选中项,支持 indeterminate 状态。
- 行复选框:原生 input checkbox,零组件开销,事件隔离避免冒泡干扰。
- 选中状态:通过 computed 维护 selectedRowKeySet 与 rowByKey,onChange 回传 keys 与 rows。
- 选择列在 displayColumns 中被插入最前列,并在 onColumnsUpdate 时被剥离,避免污染父组件 columns。
章节来源
依赖关系分析
- FmsTable.vue 依赖 StkTable 提供虚拟滚动、选区、行拖拽等能力。
- createCellRenderer.js 根据 editingActive 与列配置动态注入 customCell,降低渲染成本。
- createEditableCellRenderer.js 提供输入、选择、复选三种编辑器,并通过 WeakMap 管理草稿,避免不必要的响应式更新。
- createSelectionCellRenderers.js 提供选择列,封装选中状态与回调。
classDiagram
class FmsTable {
+props
+emits
+methods
}
class CreateCellRenderer {
+resolveCustomCell()
}
class CreateEditableCellRenderer {
+createEditableCell()
}
class CreateSelectionCellRenderers {
+createSelectionColumn()
}
FmsTable --> CreateCellRenderer : "使用"
CreateCellRenderer --> CreateEditableCellRenderer : "调用"
FmsTable --> CreateSelectionCellRenderers : "使用"
图表来源
- FmsTable.vue:273-291
- createCellRenderer.js:208-228
- createEditableCellRenderer.js:195-211
- createSelectionCellRenderers.js:18-129
章节来源
- FmsTable.vue:273-291
- createCellRenderer.js:208-228
- createEditableCellRenderer.js:195-211
- createSelectionCellRenderers.js:18-129
性能考量
- 捕获阶段拦截键盘事件:@keydown.capture 在捕获阶段处理导航键,避免子组件先消费事件,减少额外冒泡开销。
- 编辑器按需注入:editingActive 为 false 时,可编辑列走原生 td 渲染路径,显著减少函数式组件数量,提升滚动性能。
- 草稿机制:输入框使用 WeakMap 管理草稿,仅在 blur 或提交时写入真实数据,避免频繁响应式更新。
- 防抖与合并:
- 尺寸变化使用 ResizeObserver 与 trailing 防抖,避免动画期间频繁 initVirtualScroll。
- 编辑器激活使用短防抖合并快速连按,避免每键挂载/卸载整行编辑器。
- 选择列零组件开销:使用原生 input checkbox 与 CSS 样式模拟,避免每行创建 Checkbox 组件实例。
[本节为通用性能建议,无需特定文件引用]
故障排查指南
- 键盘导航无效:
- 检查 NAVIGATION_KEYS 是否包含所需键。
- 确认 @keydown.capture 已绑定到根元素。
- 验证 handleKeydownCapture/handleKeyupCapture 是否正确添加/移除监听。
- 编辑器未激活:
- 检查 editingActive 与 resolveCustomCell 是否注入 customCell。
- 确认 area-selection-change 正确传递 ranges 并定位到目标行/列。
- 值变更未上报:
- 检查 shouldNotify 逻辑(select/checkbox 使用严格相等,其他类型字符串化比较)。
- 确认 ctx.onChange 已 emit('change')。
- 取消操作无效:
- 输入框 Escape 应触发 blur,丢弃草稿。
- 复选框 mousedown 应阻止冒泡,避免与表格行为冲突。
章节来源
结论
FMS 表格通过捕获阶段拦截键盘事件、按需注入编辑器、草稿机制与防抖合并等手段,实现了高效、流畅的交互体验。area-selection-change、row-order-change、change 等事件贯穿选区、排序与编辑全流程,配合 createCellRenderer 与 createEditableCellRenderer 的协作,既保证了功能完整性,又兼顾了性能表现。遵循本文的最佳实践,可在复杂场景下稳定地扩展与定制表格行为。
[本节为总结性内容,无需特定文件引用]
附录
- 关键 API 与事件:
- area-selection-change:选区变化回调,用于激活编辑与聚焦。
- row-order-change:行拖拽排序回调,用于重排数据。
- change:数据变更回调,用于父组件同步与持久化。
- 键盘导航键集合:NAVIGATION_KEYS = {ArrowUp, ArrowDown, ArrowLeft, ArrowRight, Tab}。
- 编辑器类型:Input、Select、Checkbox,分别对应不同提交与取消策略。
[本节为补充信息,无需特定文件引用]