从 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 组件的视图显示第二个输入的旧值。
防御
该应用程序具有以下定义:
- 所有组件使用变更检测策略OnPush。包括根组件。
- 我正在使用 ngrx 商店。在减速器内部调用 markForCheck() 或 detectChanges() 不是一个选项。
- 数据使用异步管道向下传输到 child 组件,因为它来自使用选择器(可观察)的存储。
- 我不能更早地引起第二个输入的变化(例如在 parent 组件)- 它必须在 child 组件接收到第一个输入的变化之后发生。
为了说明这个案例,我创建了一个简单的演示项目。
在那里我不使用商店,所以我没有任何选择器..相反我使用 rxjs Subject 来使用异步管道。
这里是:
https://angular-ccejq4.stackblitz.io
查看控制台以了解发生了什么以及何时发生。
糟糕的解决方案
我已经尝试过的:
- 将根组件的更改检测策略更改为默认值。它有效,但这不是一个好的解决方案,因为它会导致性能问题。
- 在 emit(!) 之前和之后调用 markForCheck() - 不工作。
- 在 emit(!) 之前和之后调用 detectchanges() - 不工作。
- 订阅 parent 组件的选择器而不是使用异步管道 - 不工作。
- 从 setTimeout 中调用发出 - 它有效。但是有更好的解决方案吗?一个 Angular-driven 一个。因为这个解决方案感觉更像是一种解决方法。
- 使用 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);
}
}
我在使用 Angular 6 应用程序时遇到问题:
问题
假设我有 2 个组件:parent 和 child。
child 有 2 个输入。 当 1 个输入发生变化时,在 ngOnChanges() 内部 child 组件向 parent 组件发出一些东西。
然后 parent 组件更改 child 组件的第二个输入。 我希望再次调用 child 组件的更改检测 - 但事实并非如此。 child 组件的视图显示第二个输入的旧值。
防御
该应用程序具有以下定义:
- 所有组件使用变更检测策略OnPush。包括根组件。
- 我正在使用 ngrx 商店。在减速器内部调用 markForCheck() 或 detectChanges() 不是一个选项。
- 数据使用异步管道向下传输到 child 组件,因为它来自使用选择器(可观察)的存储。
- 我不能更早地引起第二个输入的变化(例如在 parent 组件)- 它必须在 child 组件接收到第一个输入的变化之后发生。
为了说明这个案例,我创建了一个简单的演示项目。 在那里我不使用商店,所以我没有任何选择器..相反我使用 rxjs Subject 来使用异步管道。
这里是:
https://angular-ccejq4.stackblitz.io
查看控制台以了解发生了什么以及何时发生。
糟糕的解决方案
我已经尝试过的:
- 将根组件的更改检测策略更改为默认值。它有效,但这不是一个好的解决方案,因为它会导致性能问题。
- 在 emit(!) 之前和之后调用 markForCheck() - 不工作。
- 在 emit(!) 之前和之后调用 detectchanges() - 不工作。
- 订阅 parent 组件的选择器而不是使用异步管道 - 不工作。
- 从 setTimeout 中调用发出 - 它有效。但是有更好的解决方案吗?一个 Angular-driven 一个。因为这个解决方案感觉更像是一种解决方法。
- 使用 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);
}
}