使用OnPush变更检测策略时,模板中属性值为何不更新?
问题代码
Component.ts
@Component({ selector: 'app-sample', templateUrl: './sample.component.html', styleUrls: ['./sample.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) export class SampleComponent extends SortableComponent implements OnInit { public countNumbers: number = 0; public sampleItems; constructor(private sampleService: SampleService,) { super(); } ngOnInit() { this.refreshList(); } private refreshList() { this.numberOfAssetsLeftToLoad = this.numberOfAssetsLeftToLoad + 1; this.sampleService .getSample() .then( (response: any) => { this.numberOfAssetsLeftToLoad = this.numberOfAssetsLeftToLoad - 1; this.sampleItems = response; }, (reason: Error) => { this.numberOfAssetsLeftToLoad = this.numberOfAssetsLeftToLoad - 1; console.log(reason); }, ); } }
Template
<div class="col-sm pt-4" *ngIf="numberOfAssetsLeftToLoad !== 0"> <loader loadingText="Loading Sample..." cssClass="w-50"></loader> </div> <div class="col-sm pt-4" *ngIf="numberOfAssetsLeftToLoad === 0"> <div *ngFor="let item of sampleItems;">{{item}}</div> </div>
问题描述
启用ChangeDetectionStrategy.OnPush后,模板中的numberOfAssetsLeftToLoad始终显示为1,加载状态无法结束——TS文件中属性值已更新,但模板未同步变化。
原因分析
OnPush策略下,Angular仅在以下场景触发变更检测:
- 组件
@Input属性的引用发生变化 - 组件/子组件触发DOM事件(如click、input)
- 手动调用变更检测方法
- 通过
async管道订阅的Observable流发出新值
而代码中Promise的回调属于异步操作,Angular不会自动为其触发变更检测,导致模板无法感知到numberOfAssetsLeftToLoad和sampleItems的更新。
解决方法
方法1:手动触发变更检测
注入ChangeDetectorRef,在Promise回调修改属性后主动通知Angular检测变更:
import { ChangeDetectorRef } from '@angular/core'; // ... constructor( private sampleService: SampleService, private cdr: ChangeDetectorRef // 注入变更检测引用 ) { super(); } private refreshList() { this.numberOfAssetsLeftToLoad = this.numberOfAssetsLeftToLoad + 1; this.sampleService .getSample() .then( (response: any) => { this.numberOfAssetsLeftToLoad = this.numberOfAssetsLeftToLoad - 1; this.sampleItems = response; this.cdr.detectChanges(); // 触发变更检测 }, (reason: Error) => { this.numberOfAssetsLeftToLoad = this.numberOfAssetsLeftToLoad - 1; console.log(reason); this.cdr.detectChanges(); // 错误分支同步触发 }, ); }
方法2:改用Observable + async管道(推荐)
将Promise转为Observable,利用async管道自动处理变更检测,同时把加载状态封装为可观察对象:
import { BehaviorSubject } from 'rxjs'; import { finalize } from 'rxjs/operators'; // ... export class SampleComponent extends SortableComponent implements OnInit { public countNumbers: number = 0; public sampleItems; public isLoading$ = new BehaviorSubject(true); constructor(private sampleService: SampleService,) { super(); } ngOnInit() { this.refreshList(); } private refreshList() { this.isLoading$.next(true); this.sampleService .getSample() .pipe( finalize(() => this.isLoading$.next(false)) // 请求结束后标记加载完成 ) .subscribe( (response: any) => { this.sampleItems = response; }, (reason: Error) => { console.log(reason); }, ); } }
模板同步修改:
<div class="col-sm pt-4" *ngIf="isLoading$ | async"> <loader loadingText="Loading Sample..." cssClass="w-50"></loader> </div> <div class="col-sm pt-4" *ngIf="!(isLoading$ | async)"> <div *ngFor="let item of sampleItems;">{{item}}</div> </div>
方法3:用NgZone包裹Promise回调
Angular变更检测运行在NgZone环境内,手动将Promise回调包裹到NgZone中,让Angular感知到异步操作:
import { NgZone } from '@angular/core'; // ... constructor( private sampleService: SampleService, private ngZone: NgZone ) { super(); } private refreshList() { this.numberOfAssetsLeftToLoad = this.numberOfAssetsLeftToLoad + 1; this.sampleService .getSample() .then( (response: any) => { this.ngZone.run(() => { this.numberOfAssetsLeftToLoad = this.numberOfAssetsLeftToLoad - 1; this.sampleItems = response; }); }, (reason: Error) => { this.ngZone.run(() => { this.numberOfAssetsLeftToLoad = this.numberOfAssetsLeftToLoad - 1; }); console.log(reason); }, ); }
内容的提问来源于stack exchange,提问作者Narek Keshishyan
相关产品推荐
相关产品推荐

