如何为mat-tree组件的大量子节点实现虚拟滚动?
解决Mat-Tree展开大量子节点卡顿的虚拟滚动实现方案
核心方案
Angular CDK的虚拟滚动组件(CdkVirtualScroll)可以适配树形结构,核心是给每个展开的文件夹节点的子列表单独添加虚拟滚动容器,只渲染可视区域内的节点,避免一次性渲染大量DOM元素导致的卡顿。
实现步骤
- 引入CDK滚动模块
在你的模块文件中导入ScrollingModule:
import { ScrollingModule } from '@angular/cdk/scrolling'; @NgModule({ imports: [ // 其他模块 ScrollingModule ] }) export class YourModule { }
- 改造模板结构
调整原有的mat-tree模板,为文件夹节点的子节点区域添加虚拟滚动容器,并优化层级缩进的实现(用CSS代替*ngFor生成span,减少DOM元素):
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl"> <!-- 文件节点模板 --> <mat-tree-node *matTreeNodeDef="let node" class="tree-node"> <div fxLayout="row" fxFlex> <div class="indent" [style.margin-left.px]="treeControl.getLevel(node) * 24"></div> <mat-checkbox [checked]="node.checked" (change)="onCheckboxChange(node)" [disabled]="!node.checkeable" (click)="$event.stopPropagation()" > <button mat-icon-button><mat-icon> text_snippet </mat-icon></button> </mat-checkbox> <span class="file-name" fxLayoutAlign="center center" (click)="downloadFile(node.file_ancillary_oid, node.size)" > {{ getFileName(node) }} </span> </div> <div fxFlex="20" fxLayoutAlign="center center">{{ formatSize(node.size) }}</div> </mat-tree-node> <!-- 文件夹节点模板 --> <mat-tree-node *matTreeNodeDef="let node; when: isFolder" class="tree-node border-top"> <div fxLayout="row" fxFlex> <div class="indent" [style.margin-left.px]="treeControl.getLevel(node) * 24"></div> <mat-checkbox [checked]="node.checked" (change)="onCheckboxChange(node)" [disabled]="!node.checkeable"> <button mat-icon-button matTreeNodeToggle [attr.aria-label]="'toggle ' + node.name"> <mat-icon> {{ treeControl.isExpanded(node) ? 'folder_open' : 'folder' }} </mat-icon> </button> </mat-checkbox> <span fxLayoutAlign="center center" matTreeNodeToggle class="file-name"> {{ node.name }} </span> </div> <div fxFlex="20" fxLayoutAlign="center center">{{ node.size }}</div> <!-- 虚拟滚动容器包裹子节点 --> <div class="child-container"> <cdk-virtual-scroll-viewport itemSize="48" class="virtual-scroll-viewport"> <mat-tree-node *matTreeNodeDef="let child; when: hasChild" matTreeNodeOutlet></mat-tree-node> <mat-tree-node *matTreeNodeDef="let child" matTreeNodeOutlet></mat-tree-node> </cdk-virtual-scroll-viewport> </div> </mat-tree-node> </mat-tree>
- 添加CSS样式
给虚拟滚动容器和节点添加必要的样式,确保容器有固定高度,节点布局正确:
.tree-node { height: 48px; /* 要和itemSize一致 */ display: flex; align-items: center; } .virtual-scroll-viewport { height: 300px; /* 可视区域高度,可根据需求调整 */ width: 100%; } .child-container { margin-left: 24px; /* 对应层级缩进的距离 */ } .indent { width: 24px; }
- 组件类调整
确保你的TreeControl和DataSource支持按需获取子节点,如果是一次性加载所有数据,需要确保dataSource的connect方法返回的流能正确响应节点展开/折叠事件:
// 示例:如果用FlatTreeControl和FlatTreeDataSource treeControl = new FlatTreeControl<YourNodeType>( node => node.level, node => node.expandable ); dataSource = new MatTreeFlatDataSource(this.treeControl, new YourTreeDataSource()); // 确保hasChild方法正确判断 hasChild = (_: number, node: YourNodeType) => node.expandable;
关键说明
- itemSize设置:
itemSize必须和节点的实际高度一致,这样虚拟滚动才能准确计算可视区域内的节点数量。 - 层级缩进优化:用
style.margin-left代替*ngFor生成span,减少不必要的DOM元素,提升性能。 - 动态加载子节点:如果你的文件夹子节点数量极大,可以结合懒加载,在节点展开时才请求后端数据,进一步减少初始加载压力。
内容的提问来源于stack exchange,提问作者Ramón Parejo Cuéllar
相关产品推荐
相关产品推荐

