奇怪的变化检测行为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),
);
或者你也可以tick
appRef
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 会触发变化检测吗?
详解ANGULAR2组件中的变化检测机制(对比ANGULAR1的脏检测)