奇怪的变化检测行为Angular 2+

Posted

技术标签:

【中文标题】奇怪的变化检测行为Angular 2+【英文标题】:Odd change detection behavior Angular 2+ 【发布时间】:2019-06-20 12:24:17 【问题描述】:

我正在开发一个大型应用程序,但在变更检测方面遇到了一些问题。

父组件ts:

使用changeDetection: ChangeDetectionStrategy.OnPush

我有一个可观察的变量

loaderOverlay$: Observable<boolean>;

this.loaderOverlay$ = this.store.pipe(
  select(selectors.loaderOverlaySelector)
);

此变量从子组件的 rxjs 操作中更新。然后通过 rxjs 进程。 (动作 -> 减速器 -> 选择器)

父组件 HTML

<div *ngif="(loaderOverlay$ | async)"></div>

Child #1 组件(我正在调度我的操作):

myFunction() 
  this.store.dispatch(new actions.LoaderOverlay(true));


我的问题是,一旦我发送动作,*ngif 就会非常不稳定。它似乎没有按照我想要的方式工作(调度操作,将值更改为 true 以便出现 div)。这很奇怪,因为如果我在减速器中console.log(action.payload),值实际上正在更新,但*ngif 不起作用。更奇怪的是,当我将鼠标悬停在某个其他组件上时,它似乎启动了。

我认为我已经将其范围缩小到更改检测,因为如果我这样做,则在父组件中:

ngAfterViewChecked()
   this.changeDetector.detectChanges();

这似乎对我有用。我的问题是ngAfterViewChecked 似乎被触发了很多次,我担心性能问题。

这里可能发生了什么,我可以做些什么来解决这个奇怪的问题?

【问题讨论】:

你真的是这样调度那个动作的吗?也许你从回调或其他什么地方调度它?因为看起来你在 NgZone 之外进行调度和操作,所以它没有被检查。 selectors.loaderOverlaySelector 是什么?不要将状态分配给loaderOverlay$,而是尝试订阅以查看您得到的this.store.pipe( select(selectors.loaderOverlaySelector).subscribe(value => console.log(value) ) ); 好吧,我现在很好奇你在哪里使用 myFunction()?是来自子组件的模板吗? 请添加动作、存储和效果代码,以便我们看到大图,如果你能制作一个很棒的 stackblitz 示例:) @cup_of 是的,async 管道可以做到这一点,但仅用于调试目的。弄清楚如何解决问题后,就可以清理了。 【参考方案1】:

您的loaderOverlay$ 发出的值是否评估为true?我在 stackblitz 上创建了一个类似的代码(如您所述),但我似乎无法重现您遇到的错误。它要么是在 angular/ngrx 中修复的错误,要么是您发出的值未评估为 true(选择不好并返回 undefined 或其他东西)。尝试点击 observable 并记录结果。

https://stackblitz.com/edit/angular-ngrx-update?file=src/app/app.component.css

后期编辑:

正如 Cristian Traina 在 cmets 中指出的那样,markForCheck(来自异步管道内部)在从外部角度调用时不会触发更改检测。作为修复,您可以在管道中使用区域调度程序,或者检测更改的来源并修补该区域以触发 Angular 内部的更改检测(因为您的应用程序的其他区域可能存在问题)。

https://www.npmjs.com/package/ngx-zone-scheduler?activeTab=readme

public constructor(
  @Inject(ZoneScheduler)
  private readonly zoneScheduler: SchedulerLike,
) 

this.loaderOverlay$ = this.store.pipe(
  select(selectors.loaderOverlaySelector),
  observeOn(this.zoneScheduler),
);

或者你也可以tickappRef

public constructor(
  private appRef: ApplicationRef,
) 

this.loaderOverlay$ = this.store.pipe(
  select(selectors.loaderOverlaySelector),
  tap(_ => this.appRef.tick()),
);

【讨论】:

正如我在评论中所说,这可能是与 Zone 相关的问题 @CristianTraìna 我对此表示怀疑......我在 stackblitz 示例中添加了 Zone 外的调度,它工作正常。 @CristianTraìna 你是对的......我希望 markForCheck 能够在 Angular 内部运行,即使它在外部调用...... 您的解决方案没问题,但听起来像是一种解决方法。 Observables 是区域友好的,所以它们应该自己触发变化检测。真正的问题在别处,也许他在某处使用谷歌地图,这也是你无法在你的 sn-p 中重现 bug 的原因 @CristianTraìna 我能够使用 setTimeout ouside angular 重现它。 stackblitz.com/edit/…【参考方案2】:

我遇到了类似的问题。通过编辑输入字段,将从后端获取结果并显示在列表中。除非我将鼠标悬停在其他地方或单击其他地方,否则该列表不会更新。

我通过在从后端获取数据后添加手动更改检测来修复它。

constructor(private cd: ChangeDetectorRef) 

fetchData.subscribe(results => 
   AddResultsToStore(results);
   this.cd.detectChanges();
);

【讨论】:

这无济于事!当可观察对象发出新值时,async 管道已经将组件标记为脏......他的事件很可能是在角度区域之外发生的。 可能!我不知道他的确切设置。我只是把它写下来,以防它有助于解决他的问题。它在我的代码中解决了一个特定的类似问题,我不建议在每次通话后都使用它。【参考方案3】:

从您的示例看来,onPush 策略似乎没有被正确使用。 onPush 策略意味着如果它的@inputs 没有改变,即使组件中的变量正在更新,组件也不一定会更新。根据我在您的示例中收集到的信息,父组件的 @input 引用没有更改,这意味着更改检测没有按您的预期发生。

要解决此问题,我建议更新父组件(订阅商店更新)以使用defaultStrategy 或根据需要手动调用markForCheck() 来触发更新。

【讨论】:

他正在使用异步管道,隐式执行 markForCheck()。所以 IMO 这不可能是原因 @LlorençPujolFerriol 如果他没有对父级的输入使用异步函数,我认为不会隐式调用 markForCheck()。如果父项的输入没有改变,则不会调用 markForCheck @JusMalcolm 如果您查看异步管道代码,它会在新事件通过 Observable 时调用 markForCheck。 github.com/angular/angular/blob/…

以上是关于奇怪的变化检测行为Angular 2+的主要内容,如果未能解决你的问题,请参考以下文章

Angular 2,Eventemitter 会触发变化检测吗?

Angular 2 变化检测与 observables

Angular 2:检测浏览器宽度的变化并更新模型

详解ANGULAR2组件中的变化检测机制(对比ANGULAR1的脏检测)

详解angular2组件中的变化检测机制(对比angular1的脏检测)

Angular 2:如何检测数组中的变化? (@input 属性)