问题现象
在使用 Material Table 时,排序功能触发了一个奇怪的 Bug:表格的 Row 无法展开。最终排查发现,问题的根源在于 `trackBy` 的错误使用。`trackBy` 方法接受两个参数:`index`(数据索引)和 `row`(当前行的数据)。在实现中,我们只使用了 `row` 参数,并返回了 `row.id` 作为唯一标识。但在排序的场景下,这种实现会导致渲染错误。
解决 Bug 的过程
初步排查
因为问题是在排序后出现的,我们最先检查了排序的函数实现。经过反复测试,发现排序逻辑是正确的,Bug 并不是由此引发。
接着,我们怀疑可能是 Material Table 的用法有误,于是对照官方文档检查了表格的用法配置,结果发现一切符合规范,没有发现明显的问题。
尝试解决
为了进一步缩小排查范围,我们尝试调整表格的渲染逻辑,将多个 Row 类型精简为单一类型。
调整前:
<!-- Parent Row --> <tr mat-row *matRowDef="let row; columns: getColumns(context.columns);" (click)="toggleRow(row, $event)" [ngStyle]="row.style?.row" class="parent-refer" [attr.row-id]="row.id"> </tr> <!-- Child Row --> <tr mat-row *matRowDef="let row; columns: getColumns(context.columns); when: isChildRows;" [@detailExpand]="isRowExpanded(row) ? 'expanded' : 'collapsed'" [ngStyle]="row.style?.row" class="child-refer" [attr.row-id]="row.id"> </tr>
调整后:
<!-- Single Row --> <tr mat-row *matRowDef="let row; columns: getColumns(context.columns);" (click)="toggleRow(row, $event)" [ngStyle]="row.style?.row" class="parent-refer" [attr.row-id]="row.id"> </tr>
测试结果
Bug 确实解决了,表格排序后可以正常展开。但我们很快放弃了这个方案,主要基于以下两点考虑:
- 影响范围不明确 该组件被多处复用,删除 Child Row 的渲染逻辑可能会导致其他功能异常。
- 官方文档推荐 Material Table 官方文档明确建议使用两个 Row(Parent Row 和 Child Row)来实现分层渲染,我们希望尽量遵循最佳实践。
进一步排查
既然调整渲染方式并不符合实际需求,我们决定从渲染问题本身着手,通过浏览器开发工具(Elements 面板)检查实际渲染结果。
在对比分析中,我们发现一个关键问题:
排序后,原本应该渲染为 `Parent Row` 的内容,被错误地渲染为 `Child Row`,导致 `Parent Row` 无法展开。
从根本上看,渲染错误可能由以下几方面引起:
- 数据绑定问题
- 变更检测问题
- 数据顺序和结构问题
定位根因
经过逐一排查,最终发现问题源于 **数据顺序和结构问题**,而引发这一问题的核心是 `trackBy` 的实现。
在 `trackBy` 函数中,我们返回了 `row.id` 作为唯一标识符,但在排序的过程中,`row.id` 并不能反映数据的新位置,导致 Angular 的变更检测机制无法正确判断哪些 DOM 元素需要更新,从而引发渲染错误。
问题解决
通过修改 `trackBy` 函数,使其返回 `index` 和 `row.id` 的组合来唯一标识每一行,成功解决了这个问题:
trackByRow(index: number, row: any): any { return `${index}-${row.id}`; // 使用索引和ID的组合 }
总结
这个 Bug 的排查和解决过程让我们学到了一些重要的经验:
- trackBy 的作用 在 Angular 中,trackBy 用于优化 *ngFor 循环的性能,但使用时需要特别注意其唯一标识符的稳定性,尤其在排序、筛选等动态操作中,标识符必须能够反映数据的新状态。
- 分层排查的重要性 遇到问题时,从影响范围较小的模块(如排序函数)逐步向全局(如渲染逻辑和框架机制)排查,可以有效缩小问题范围。
- 遵循最佳实践 官方文档推荐的实现方式通常是经过深思熟虑的,应尽量在其基础上进行改进,而非彻底推翻。
通过这次问题的解决,我们不仅修复了 Bug,也加深了对 Angular 框架内部机制的理解。