从 ngOnChanges 更改组件输入,同时使用 OnPush 策略

Change component input from ngOnChanges, while using OnPush strategy

我在使用 Angular 6 应用程序时遇到问题:

问题

假设我有 2 个组件:parent 和 child。

child 有 2 个输入。 当 1 个输入发生变化时,在 ngOnChanges() 内部 child 组件向 parent 组件发出一些东西。

然后 parent 组件更改 child 组件的第二个输入。 我希望再次调用 child 组件的更改检测 - 但事实并非如此。 child 组件的视图显示第二个输入的旧值。


防御

该应用程序具有以下定义:

  1. 所有组件使用变更检测策略OnPush。包括根组件。
  2. 我正在使用 ngrx 商店。在减速器内部调用 markForCheck() 或 detectChanges() 不是一个选项。
  3. 数据使用异步管道向下传输到 child 组件,因为它来自使用选择器(可观察)的存储。
  4. 我不能更早地引起第二个输入的变化(例如在 parent 组件)- 它必须在 child 组件接收到第一个输入的变化之后发生。

为了说明这个案例,我创建了一个简单的演示项目。 在那里我不使用商店,所以我没有任何选择器..相反我使用 rxjs Subject 来使用异步管道。

这里是:

https://angular-ccejq4.stackblitz.io

查看控制台以了解发生了什么以及何时发生。


糟糕的解决方案

我已经尝试过的:

  1. 将根组件的更改检测策略更改为默认值。它有效,但这不是一个好的解决方案,因为它会导致性能问题。
  2. 在 emit(!) 之前和之后调用 markForCheck() - 不工作。
  3. 在 emit(!) 之前和之后调用 detectchanges() - 不工作。
  4. 订阅 parent 组件的选择器而不是使用异步管道 - 不工作。
  5. 从 setTimeout 中调用发出 - 它有效。但是有更好的解决方案吗?一个 Angular-driven 一个。因为这个解决方案感觉更像是一种解决方法。
  6. 使用 ngAfterViewInit/ngAfterViewChecked 并发出,从那里调用 markForCheck() 和 detectChanges() - 不工作。

我发现新值到达了异步管道。异步管道然后调用 markForCheck()。 但它不调用 detectChanges().. 所以变化检测周期不会 运行。 只有在下一个变化检测周期,视图才会正确变化——正如您在我的演示应用程序中看到的那样。


有什么想法吗?

谢谢!

首先让我说,很好的问题,详细且解释得很好。您确实遇到了 angular 变化检测的警告。

ngOnChanges 中没有直接的方法来触发另一个变化检测。否则你将有可能陷入循环。在性能方面,他们也不允许这样做。你需要想办法进入JS的下一个执行周期

setTimeout 是克服这个问题的不错选择。你可以把它放在 emit 周围:

setTimeout(() => this.secondEmitter.emit(this.first));

或者您可以将它放在 markForCheck 调用周围:

setTimeout(() => this.cdRef.markForCheck());

但我同意你的看法,感觉有点老套,但有时你只需要应付它。如果你真的做不到,还有另一种方法。您的 BehaviorSubject 本质上是一个同步可观察对象。但是,您可以在模板中进行 async 订阅以使用 asyncScheduler:

private second = new BehaviorSubject(0);

readonly second$ = this.second.asObservable().pipe(
  observeOn(asyncScheduler)
)

constructor() {
  this.second.next(0);
  this.second$.subscribe((lastval) => {
    console.log(`second subscription update to ${lastval}`);
  });
}

根据最后一个选项,我对你的 stack

进行了分叉

这将使任何订阅它的人在下一个事件循环周期后收到消息。


更新

回到这个问题上,更好的解决方案是在创建 EventEmitter.

时使用 async 选项

在这种情况下,您可以在第二个可观察对象上使用 .emit

@Output() firstEmitter:EventEmitter<number> = new EventEmitter();

// notice the 'true'
@Output() secondEmitter:EventEmitter<number> = new EventEmitter(true);

ngOnChanges(changes:any) {
  if (changes.first) {
    this.secondEmitter.emit(this.first);
  }
}

working example