Angular Material的 MatTable 组件封装了排序、分页、筛选等能力,用起来确实省事,但不少人在第一步就栽了跟头:接口返回了新数据,赋值给 dataSource.data 之后表格纹丝不动,或者刷新行为时灵时不灵。这个问题的本质在于 MatTable 的渲染机制依赖 Angular 的变更检测,一旦数据变更没有被框架感知到,表格就不会重新渲染。本文从渲染原理讲起,逐一拆解常见原因,并给出对应的解决方案。

一、先弄懂 MatTable 的刷新机制
MatTable 并不是自己监听数据的组件。它的渲染流程是:当变更检测运行到表格所在的组件树时,表格组件会把 dataSource.data 按当前排序、分页状态切成一行行的 cdk-row 渲染出来。也就是说,表格刷新的前提有两个:一是数据引用真的变了,二是 Angular 的变更检测确实跑到了这里。
第一个前提最容易踩坑。下面这段代码看似没问题,实际上多数情况是可以工作的:
// 正确:整体替换 data 引用,MatTable 能感知到变化 this.dataSource.data = newArray;
但如果你只是修改了数组内部对象的属性,比如 this.dataSource.data[0].name = '新名字',数组引用没变,每个元素还是同一个对象,MatTable 的 differ 判断不出差异,自然不会重渲染。这一点和向数组 push 元素类似:向 dataSource.data 里 push 一条记录,数组引用依旧没变,表格同样不动。
第二个前提和变更检测策略有关。如果组件装饰器上声明了 changeDetection: ChangeDetectionStrategy.OnPush,那么只有在输入属性引用变化、组件内触发了事件、或者手动调用了 markForCheck 的情况下,该组件才会被检测。在 setTimeout、第三方回调、WebSocket 消息里改数据,OnPush 组件很容易被跳过。
二、四种常见原因逐一排查
第一种:直接操作了 data 数组之外的东西。有些项目会自己保存一份 list,把 list 传给表格后又修改 list,但从未重新赋值给 dataSource.data。MatTable 只认 dataSource.data,别的数组它一概不知。解决办法很简单,改完之后统一执行一次赋值:
// 错误:修改了自己的 list,dataSource 毫不知情 this.list.push(newItem); // 正确:改动后同步给 dataSource this.list.push(newItem); this.dataSource.data = [...this.list];
第二种:异步回调里修改数据但没有触发检测。典型场景是订阅了推送消息或使用了 NgZone 之外运行的回调,比如地图 SDK、WebSocket 原生事件。此时可以手动注入变更检测服务:
constructor(private cdr: ChangeDetectorRef) {}
onWebSocketMessage(data: Item[]) {
this.dataSource.data = data;
// OnPush 组件用 markForCheck,默认策略用 detectChanges
this.cdr.markForCheck();
this.cdr.detectChanges();
}
detectChanges 和 markForCheck 的区别值得记住:前者立即在当前组件及其子组件上跑一次检测,立刻见效;后者只是把 OnPush 组件标记为待检查,等到下一轮全局变更检测时再处理。在 Zone.js 正常工作的环境里,多数情况下用 markForCheck 更符合 Angular 的设计习惯。
第三种:使用了 MatPaginator 或 MatSort 却没有把它们挂到 dataSource 上。分页切换后数据没刷新,往往是因为忘了这几行初始化代码:
@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort;
ngAfterViewInit() {
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
}
第四种:模板里 *matRowDef 绑定的列名和数据字段对不上,导致数据其实刷新了但看起来还是旧值。检查 displayedColumns、matColumnDef 和行数据字段三者是否完全一致,大小写拼写不一致是重灾区。
三、两种刷新方式的对比与推荐写法
除了重新赋值 dataSource.data,MatTable 的 dataSource 还提供了 renderRows() 方法。二者的区别是:赋值方式会经过 differ 对比,只渲染变化的部分;而 renderRows() 是强制让所有数据行重新过一遍渲染流程,适合引用没变但内容全变了的原地修改场景。如果业务代码大量使用原地修改,比如直接改对象属性,可以封装一个统一的刷新方法:
refreshTable() {
// 方式一:拷贝数组替换引用,推荐
this.dataSource.data = [...this.list];
// 方式二:强制重渲染,适合原地修改较多的场景
// this.dataSource.renderRows();
this.cdr.markForCheck();
}
更彻底的方案是让数据源本身变成响应式流。用 BehaviorSubject 加接口请求的组合,数据一到就自动推给表格,业务层完全不需要关心刷新时机:
private dataSubject = new BehaviorSubject<Item[]>([]);
dataSource = new MatTableDataSource<Item>();
ngOnInit() {
this.dataSubject.subscribe(data => this.dataSource.data = data);
}
reload(params: QueryParams) {
this.http.get<Item[]>('/api/items', { params })
.subscribe(res => this.dataSubject.next(res));
}
这种写法的好处是把取数和渲染解耦,任何地方调用 reload 表格都会更新,分页筛选逻辑也可以统一挂在 reload 里,避免多处赋值导致的遗漏。配合 async 管道使用时,Angular 会自动管理订阅和检测,连手动调用 ChangeDetectorRef 都省了。
最后总结一下排查顺序:先确认改的是 dataSource.data 而不是别的数组,再确认引用被替换而非原地修改,然后检查组件是否 OnPush 以及回调是否脱离了变更检测,最后核对分页排序是否挂载、列名是否对齐。按这个链路走一遍,绝大多数表格不刷新的问题都能当场定位。
Angular Material Table数据源更新ChangeDetectorRef修改时间:2026-09-14 21:33:08