NGXS Store结合RXJS处理数据渲染时遇UI无限循环冻结问题
解决Angular+NGXS中动态转换Store数据导致UI冻结的问题
问题根源
Angular的变更检测机制会在每次检测周期调用模板中的方法(getPrintStyleGroups()),而每次调用都会返回全新的Observable实例。Async管道会对每个新Observable重新订阅,触发新一轮变更检测,最终形成无限循环,导致UI冻结。
正确解决方案
1. 将转换后的Observable定义为组件属性
不要在模板中直接调用方法生成Observable,而是在组件初始化阶段创建一次转换后的Observable,避免重复创建和订阅。
2. 修正RXJS转换逻辑
原代码中from(this.printStyles$)是错误的(会把Observable包装成Observable的Observable),直接基于printStyles$进行转换即可。同时如果需要响应Store数据的更新,不要使用first()(它只会取第一个值后就完成流)。
方案一:使用数组reduce分组(更简洁)
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Observable, of, Subject } from 'rxjs'; import { filter, switchMap, takeUntil } from 'rxjs/operators'; import { Store } from '@ngxs/store'; import { PrintStyleState, PaperFormatDto } from './your-state-file'; @Component({ // ...组件元数据 }) export class YourComponent implements OnInit, OnDestroy { printStyles$: Observable<PaperFormatDto[]> = this.store.select(PrintStyleState.getPrintStyles); printStyleGroups$: Observable<{ groupKey: string, items: PaperFormatDto[] }[]>; private destroy$ = new Subject<void>(); constructor(private store: Store) {} ngOnInit(): void { this.printStyleGroups$ = this.printStyles$.pipe( // 过滤空值/undefined filter(styles => !!styles), // 转换分组逻辑 switchMap(styles => { // 用reduce对数组按group分组 const groupedMap = styles.reduce((acc, style) => { const groupKey = style.group; if (!acc[groupKey]) { acc[groupKey] = []; } acc[groupKey].push(style); return acc; }, {} as Record<string, PaperFormatDto[]>); // 转换成组件需要的数组格式 const groupedArray = Object.entries(groupedMap).map(([key, items]) => ({ groupKey: key, items: items })); return of(groupedArray); }), // 组件销毁时终止订阅,防止内存泄漏 takeUntil(this.destroy$) ); } ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); } }
方案二:使用RXJS groupBy操作符
如果偏好RXJS链式操作,修正原有的管道逻辑:
import { from } from 'rxjs'; import { zip, of } from 'rxjs'; import { filter, switchMap, groupBy, mergeMap, toArray, map, takeUntil } from 'rxjs/operators'; // ...组件其他代码 ngOnInit(): void { this.printStyleGroups$ = this.printStyles$.pipe( filter(styles => !!styles), // 将数组转为单值流 switchMap(styles => from(styles)), // 按group分组 groupBy(style => style.group), // 合并分组的key和对应的数组 mergeMap(group => zip(of(group.key), group.pipe(toArray()))), // 转换为统一格式 map(([groupKey, items]) => ({ groupKey, items })), // 收集所有分组为数组 toArray(), takeUntil(this.destroy$) ); }
3. 模板中直接使用属性
修改模板,直接绑定预定义的printStyleGroups$属性,避免每次变更检测调用方法:
<nb-select fullWidth id="paperFormatInput" name="paperFormat" placeholder="Paper Format"> <nb-option-group *ngFor="let group of printStyleGroups$ | async" [title]="group.groupKey"> <nb-option *ngFor="let style of group.items" [value]="style.id"> {{ style.name }} </nb-option> </nb-option-group> </nb-select>
额外注意事项
- 如果不需要响应Store数据的后续更新,可以在管道中添加
first(),但要确保数据加载完成后不再变更。 - 始终用
takeUntil在组件销毁时清理订阅,避免内存泄漏。
内容的提问来源于stack exchange,提问作者Marcel Rückert
相关产品推荐
相关产品推荐

