Subject订阅中值变更时NgIf指令未触发的问题求助
Hey,我之前开发Angular时也碰到过一模一样的问题,咱们来一步步拆解原因和解决办法:
为啥*ngIf没触发?
核心问题在于Subject的订阅回调可能跑在了Angular的变更检测周期外面。如果触发responseStateSubject.next()的代码是在Angular上下文之外(比如第三方库的异步操作、原生JS事件、setTimeout这类),Angular根本不知道responseState已经变了,所以模板里的*ngIf就不会更新——哪怕控制台已经打出了正确的值。
另外快速确认下:你的组件里responseState是不是声明成public属性了?要是写成private的话模板根本访问不到,但从控制台能打印来看这个概率不大,不过还是得确保它是公开的,比如:
public responseState: RequestResponseState;
可行的解决办法
办法1:用NgZone把回调拉回Angular周期
Angular的NgZone服务能帮我们把代码拽回变更检测的范围内,步骤很简单:
- 先在组件里注入NgZone:
import { NgZone } from '@angular/core'; constructor(private ngZone: NgZone) {}
- 把赋值
responseState的逻辑包在ngZone.run()里:
this.responseStateSubject.subscribe((x) => { this.ngZone.run(() => { this.responseState = x as RequestResponseState; console.log(this.responseState); }); // 剩下的DOM操作逻辑可以留在外面,也可以放进去,不影响效果 if (x != undefined && x != null) { this.mainWrapper.nativeElement.classList.add("open-main-wrapper"); if (x.requestEnum != this.requestResultBaseEnum.Loading) { this.utilsS.scrollElementIntoView("response-main-wrapper"); } } else { if (this.mainWrapper != null) { this.mainWrapper.nativeElement.classList.remove( "open-main-wrapper" ); } } });
办法2:改用AsyncPipe(强烈推荐)
AsyncPipe是Angular专门为Observable/Subject做的工具,它会自动帮你处理订阅、取消订阅,还会自动触发变更检测,代码更简洁还能避免内存泄漏:
- 组件里不用手动订阅,直接把输入的Subject转成Observable:
import { tap } from 'rxjs/operators'; @Input() responseStateSubject: Subject<RequestResponseState>; public responseState$: Observable<RequestResponseState>; ngOnInit(): void { this.responseState$ = this.responseStateSubject.asObservable(); // 原来的DOM操作逻辑可以用tap操作符来处理 this.responseState$.pipe( tap(x => { if (x != undefined && x != null) { this.mainWrapper.nativeElement.classList.add("open-main-wrapper"); if (x.requestEnum != this.requestResultBaseEnum.Loading) { this.utilsS.scrollElementIntoView("response-main-wrapper"); } } else { if (this.mainWrapper != null) { this.mainWrapper.nativeElement.classList.remove( "open-main-wrapper" ); } } }) ).subscribe(); // 这里必须订阅,tap里的逻辑才会执行 }
- 模板里直接用AsyncPipe,还能避免重复订阅:
<div #mainWrapper id="response-main-wrapper" class="flex-center"> <ng-container *ngIf="responseState$ | async as responseState"> <div *ngIf="responseState != null && responseState != undefined" id="message-main-container" > <hr class="grey-hr" /> </div> </ng-container> </div>
用
ng-container把async的结果存成局部变量,这样多次使用也不会重复订阅,比直接写(responseState$ | async)更高效。
办法3:手动触发变更检测
注入ChangeDetectorRef,赋值后手动通知Angular要检查变更:
- 先注入服务:
import { ChangeDetectorRef } from '@angular/core'; constructor(private cdr: ChangeDetectorRef) {}
- 赋值后调用
markForCheck()或者detectChanges():
this.responseStateSubject.subscribe((x) => { this.responseState = x as RequestResponseState; console.log(this.responseState); // 手动标记需要检测变更 this.cdr.markForCheck(); // 剩下的逻辑不变... });
小提示:
markForCheck()是告诉Angular下次检测时要检查当前组件和父组件,detectChanges()是立即触发当前组件和子组件的检测,按需选就行。
总结
优先选办法2(AsyncPipe),完全符合Angular的最佳实践,省掉手动订阅的麻烦还能避免内存泄漏。如果因为业务限制不能用的话,办法1或3也能完美解决问题。
内容的提问来源于stack exchange,提问作者ama

