# 事件处理机制 **本文引用的文件** - [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) ## 目录 1. [简介](#简介) 2. [项目结构](#项目结构) 3. [核心组件](#核心组件) 4. [架构总览](#架构总览) 5. [详细组件分析](#详细组件分析) 6. [依赖关系分析](#依赖关系分析) 7. [性能考量](#性能考量) 8. [故障排查指南](#故障排查指南) 9. [结论](#结论) 10. [附录](#附录) ## 简介 本技术文档聚焦 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:生成选择列(含全选/单选)及内部状态管理。 ```mermaid graph TB A["FmsTable.vue"] --> B["StkTable(底层表格库)"] A --> C["createCellRenderer.js"] C --> D["createEditableCellRenderer.js"] A --> E["createSelectionCellRenderers.js"] A --> F["窗口级键盘监听
handleKeydownCapture / handleKeyupCapture"] ``` 图表来源 - [FmsTable.vue:557-584](file://fms-vue/src/components/fms-table/FmsTable.vue#L557-L584) - [createCellRenderer.js:208-228](file://fms-vue/src/components/fms-table/utils/createCellRenderer.js#L208-L228) - [createEditableCellRenderer.js:195-211](file://fms-vue/src/components/fms-table/utils/createEditableCellRenderer.js#L195-L211) - [createSelectionCellRenderers.js:18-129](file://fms-vue/src/components/fms-table/utils/createSelectionCellRenderers.js#L18-L129) 章节来源 - [FmsTable.vue:1-800](file://fms-vue/src/components/fms-table/FmsTable.vue#L1-L800) ## 核心组件 - 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](file://fms-vue/src/components/fms-table/FmsTable.vue#L10-L39) - [createCellRenderer.js:208-228](file://fms-vue/src/components/fms-table/utils/createCellRenderer.js#L208-L228) - [createEditableCellRenderer.js:195-211](file://fms-vue/src/components/fms-table/utils/createEditableCellRenderer.js#L195-L211) - [createSelectionCellRenderers.js:18-129](file://fms-vue/src/components/fms-table/utils/createSelectionCellRenderers.js#L18-L129) ## 架构总览 下图展示了表格事件从 DOM 到业务层的流转路径,包括捕获阶段的键盘导航、选区变化、行拖拽排序以及编辑变更通知。 ```mermaid 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](file://fms-vue/src/components/fms-table/FmsTable.vue#L413-L448) - [FmsTable.vue:451-505](file://fms-vue/src/components/fms-table/FmsTable.vue#L451-L505) - [FmsTable.vue:310-314](file://fms-vue/src/components/fms-table/FmsTable.vue#L310-L314) - [createCellRenderer.js:208-228](file://fms-vue/src/components/fms-table/utils/createCellRenderer.js#L208-L228) - [createEditableCellRenderer.js:66-70](file://fms-vue/src/components/fms-table/utils/createEditableCellRenderer.js#L66-L70) ## 详细组件分析 ### 键盘导航事件处理(捕获与释放) - 捕获阶段拦截:在根元素上使用 @keydown.capture 将方向键/Tab 的捕获阶段交给 handleKeydownCapture,确保在子组件(如输入框)之前优先处理导航逻辑。 - 导航键集合:NAVIGATION_KEYS = {ArrowUp, ArrowDown, ArrowLeft, ArrowRight, Tab}。 - 按下处理: - 首次按下时,动态在 window 上注册 keyup 捕获监听与 blur 监听,用于后续释放处理与失焦清理。 - 记录 pressedNavigationKeys 集合以跟踪当前按下的导航键数量。 - 使用 event.repeat 判断是否为按住重复触发,设置 isRepeatNavigation 标志,用于区分“长按滚动”和“离散点按”。 - 释放处理: - handleKeyupCapture 在对应键释放时从集合移除;当集合为空时移除所有监听并进入最终激活逻辑。 - 若为长按结束,立即 commitPendingEditing,不再等待防抖。 - 若为快速连按,使用 scheduleEditActivation 进行短防抖合并,避免频繁挂载/卸载编辑器导致掉帧。 - 失焦处理:handleWindowBlur 清理监听与状态,确保异常退出编辑态。 ```mermaid 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 ``` 图表来源 - [FmsTable.vue:98-102](file://fms-vue/src/components/fms-table/FmsTable.vue#L98-L102) - [FmsTable.vue:413-448](file://fms-vue/src/components/fms-table/FmsTable.vue#L413-L448) - [FmsTable.vue:373-406](file://fms-vue/src/components/fms-table/FmsTable.vue#L373-L406) 章节来源 - [FmsTable.vue:98-102](file://fms-vue/src/components/fms-table/FmsTable.vue#L98-L102) - [FmsTable.vue:413-448](file://fms-vue/src/components/fms-table/FmsTable.vue#L413-L448) - [FmsTable.vue:373-406](file://fms-vue/src/components/fms-table/FmsTable.vue#L373-L406) ### 选区变化事件(area-selection-change) - 触发时机:用户在表格中进行区域选择(鼠标拖选或键盘扩展选区)时,StkTable 发出 area-selection-change。 - 处理逻辑: - 获取选区最后一个范围的 end 行列索引,定位目标行与列。 - 若存在 editor 配置的列,则准备激活该行编辑,并将 focusCol 设置为该列 dataIndex。 - 键盘导航期间: - 按住方向键滚动(repeat=true):全程保持无编辑器,仅缓存 pendingEditingTarget,松手后统一激活,避免反复创建组件导致卡顿。 - 慢速离散点按:立即激活编辑器,提升跟手性。 - 快速连按(间隔 <150ms):使用短防抖合并激活,减少重渲染。 - 非键盘导航(鼠标点击):立即激活;若同一行已编辑,更新 focusCol 并临时禁用 selectOnFocus 以避免文本全选。 - 结果:通过 activateEditing 与 setCurrentRow 使目标行进入编辑态并聚焦对应编辑器。 ```mermaid 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 聚焦" ``` 图表来源 - [FmsTable.vue:451-505](file://fms-vue/src/components/fms-table/FmsTable.vue#L451-L505) - [createCellRenderer.js:208-228](file://fms-vue/src/components/fms-table/utils/createCellRenderer.js#L208-L228) 章节来源 - [FmsTable.vue:451-505](file://fms-vue/src/components/fms-table/FmsTable.vue#L451-L505) ### 行拖拽排序事件(row-order-change) - 触发时机:启用 rowDraggable 后,用户拖拽行改变顺序,StkTable 发出 row-order-change。 - 处理逻辑: - 清除当前选区,避免拖拽后选区残留。 - 获取当前表格数据源(或回退到 props.dataSource)。 - 向父组件 emit('row-order-change', [...rows], sourceIndex, targetIndex),由父组件完成数据重排与持久化。 章节来源 - [FmsTable.vue:310-314](file://fms-vue/src/components/fms-table/FmsTable.vue#L310-L314) ### 数据变更事件(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 阶段阻止事件冒泡,避免与表格选区/编辑激活冲突。 ```mermaid 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 ``` 图表来源 - [createEditableCellRenderer.js:66-70](file://fms-vue/src/components/fms-table/utils/createEditableCellRenderer.js#L66-L70) - [FmsTable.vue:217-219](file://fms-vue/src/components/fms-table/FmsTable.vue#L217-L219) 章节来源 - [createEditableCellRenderer.js:66-70](file://fms-vue/src/components/fms-table/utils/createEditableCellRenderer.js#L66-L70) - [FmsTable.vue:217-219](file://fms-vue/src/components/fms-table/FmsTable.vue#L217-L219) ### 编辑激活与取消流程 - 激活: - 通过 activateEditing(row, focusCol) 设置行编辑态,记录初始值 captureEditableValues,并标记 editingActive。 - 渲染器根据 editingActive 决定是否注入 customCell(可编辑列显示编辑器),否则走原生 td。 - setCurrentRow 静默高亮目标行,保证视觉一致。 - 取消: - 输入框 Escape 触发 blur,丢弃草稿并恢复原值。 - 切换编辑行时 deactivateRowEditing 清理旧行编辑态。 - 离开窗口或禁用 editable 时,清理监听与状态,并清空选区。 章节来源 - [FmsTable.vue:176-196](file://fms-vue/src/components/fms-table/FmsTable.vue#L176-L196) - [createCellRenderer.js:208-228](file://fms-vue/src/components/fms-table/utils/createCellRenderer.js#L208-L228) - [createEditableCellRenderer.js:107-126](file://fms-vue/src/components/fms-table/utils/createEditableCellRenderer.js#L107-L126) ### 选择列与全选逻辑 - 选择列由 createSelectionColumn 生成,包含: - 表头全选:统计当前数据中的选中项,支持 indeterminate 状态。 - 行复选框:原生 input checkbox,零组件开销,事件隔离避免冒泡干扰。 - 选中状态:通过 computed 维护 selectedRowKeySet 与 rowByKey,onChange 回传 keys 与 rows。 - 选择列在 displayColumns 中被插入最前列,并在 onColumnsUpdate 时被剥离,避免污染父组件 columns。 章节来源 - [createSelectionCellRenderers.js:18-129](file://fms-vue/src/components/fms-table/utils/createSelectionCellRenderers.js#L18-L129) - [FmsTable.vue:254-334](file://fms-vue/src/components/fms-table/FmsTable.vue#L254-L334) ## 依赖关系分析 - FmsTable.vue 依赖 StkTable 提供虚拟滚动、选区、行拖拽等能力。 - createCellRenderer.js 根据 editingActive 与列配置动态注入 customCell,降低渲染成本。 - createEditableCellRenderer.js 提供输入、选择、复选三种编辑器,并通过 WeakMap 管理草稿,避免不必要的响应式更新。 - createSelectionCellRenderers.js 提供选择列,封装选中状态与回调。 ```mermaid 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](file://fms-vue/src/components/fms-table/FmsTable.vue#L273-L291) - [createCellRenderer.js:208-228](file://fms-vue/src/components/fms-table/utils/createCellRenderer.js#L208-L228) - [createEditableCellRenderer.js:195-211](file://fms-vue/src/components/fms-table/utils/createEditableCellRenderer.js#L195-L211) - [createSelectionCellRenderers.js:18-129](file://fms-vue/src/components/fms-table/utils/createSelectionCellRenderers.js#L18-L129) 章节来源 - [FmsTable.vue:273-291](file://fms-vue/src/components/fms-table/FmsTable.vue#L273-L291) - [createCellRenderer.js:208-228](file://fms-vue/src/components/fms-table/utils/createCellRenderer.js#L208-L228) - [createEditableCellRenderer.js:195-211](file://fms-vue/src/components/fms-table/utils/createEditableCellRenderer.js#L195-L211) - [createSelectionCellRenderers.js:18-129](file://fms-vue/src/components/fms-table/utils/createSelectionCellRenderers.js#L18-L129) ## 性能考量 - 捕获阶段拦截键盘事件:@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 应阻止冒泡,避免与表格行为冲突。 章节来源 - [FmsTable.vue:413-448](file://fms-vue/src/components/fms-table/FmsTable.vue#L413-L448) - [createEditableCellRenderer.js:107-126](file://fms-vue/src/components/fms-table/utils/createEditableCellRenderer.js#L107-L126) - [createSelectionCellRenderers.js:58-85](file://fms-vue/src/components/fms-table/utils/createSelectionCellRenderers.js#L58-L85) ## 结论 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,分别对应不同提交与取消策略。 [本节为补充信息,无需特定文件引用]