Angular2 - 检查后表达式已更改 - 通过调整大小事件绑定到 div 宽度
Angular2 - Expression has changed after it was checked - Binding to div width with resize events
我已经阅读并调查了这个错误,但不确定我的情况的正确答案是什么。我知道在开发模式下,更改检测会运行两次,但我不愿意使用 enableProdMode()
来掩盖这个问题。
这是一个简单的示例,其中 table 中的单元格数量应随着 div 宽度的扩展而增加。 (请注意 div 的宽度不仅仅是屏幕宽度的函数,因此 @Media 不能轻易应用)
我的 HTML 看起来如下 (widget.template.html):
<div #widgetParentDiv class="Content">
<p>Sample widget</p>
<table><tr>
<td>Value1</td>
<td *ngIf="widgetParentDiv.clientWidth>350">Value2</td>
<td *ngIf="widgetParentDiv.clientWidth>700">Value3</td>
</tr></table>
这本身没有任何作用。我猜这是因为没有任何东西导致发生变化检测。但是,当我将第一行更改为以下内容并创建一个空函数来接收调用时,它开始工作,但偶尔我会得到 'Expression has changed after it was checked error'
<div #widgetParentDiv class="Content">
gets replaced with
<div #widgetParentDiv (window:resize)=parentResize(10) class="Content">
我最好的猜测是,通过此修改,将触发更改检测并且所有内容都开始响应,但是,当宽度快速变化时会抛出异常,因为上一次更改检测迭代比更改宽度需要更长的时间才能完成div.
- 有没有更好的方法来触发变化检测?
- 我是否应该通过函数捕获调整大小事件以确保
发生变化检测?
- 正在使用#widthParentDiv 访问
div acceptable 的宽度?
- 有没有更好的整体方案?
有关我的项目的更多详细信息,请参阅 this 类似问题。
谢谢
要解决您的问题,您只需在每次调整大小事件后获取 div
的大小并将其存储在组件 属性 中,然后在模板中使用该 属性 .这样,当第 2 轮变化检测 运行s 在开发模式下时,该值将保持不变。
我还建议使用 @HostListener
而不是将 (window:resize)
添加到您的模板。我们将使用 @ViewChild
来获取对 div
的引用。我们将使用生命周期钩子 ngAfterViewInit()
来设置初始值。
import {Component, ViewChild, HostListener} from '@angular/core';
@Component({
selector: 'my-app',
template: `<div #widgetParentDiv class="Content">
<p>Sample widget</p>
<table><tr>
<td>Value1</td>
<td *ngIf="divWidth > 350">Value2</td>
<td *ngIf="divWidth > 700">Value3</td>
</tr>
</table>`,
})
export class AppComponent {
divWidth = 0;
@ViewChild('widgetParentDiv') parentDiv:ElementRef;
@HostListener('window:resize') onResize() {
// guard against resize before view is rendered
if(this.parentDiv) {
this.divWidth = this.parentDiv.nativeElement.clientWidth;
}
}
ngAfterViewInit() {
this.divWidth = this.parentDiv.nativeElement.clientWidth;
}
}
太糟糕了,这不起作用。我们得到
Expression has changed after it was checked. Previous value: 'false'. Current value: 'true'.
错误是抱怨我们的NgIf
表达式——第一次是运行s,divWidth
是0,然后是ngAfterViewInit()
运行s并将值更改为 0 以外的值,然后是第二轮变化检测 运行s(在开发模式下)。值得庆幸的是,有一个 easy/known 解决方案,这是一个一次性问题,而不是像 OP 中那样持续存在的问题:
ngAfterViewInit() {
// wait a tick to avoid one-time devMode
// unidirectional-data-flow-violation error
setTimeout(_ => this.divWidth = this.parentDiv.nativeElement.clientWidth);
}
请注意,此处记录了这种等待一次报价的技术:https://angular.io/docs/ts/latest/cookbook/component-communication.html#!#parent-to-view-child
通常,在 ngAfterViewInit()
和 ngAfterViewChecked()
中,我们需要使用 setTimeout()
技巧,因为这些方法是在组件视图组合后调用的。
这是一个工作 plunker.
我们可以做得更好。我认为我们应该限制调整大小事件,使得 Angular 变化检测仅 运行s,比方说,每 100-250 毫秒,而不是每次发生调整大小事件时。这应该可以防止应用程序在用户调整 window 大小时变得迟钝,因为现在,每个调整大小事件都会导致更改检测到 运行(在开发模式下两次)。您可以通过在之前的 plunker 中添加以下方法来验证这一点:
ngDoCheck() {
console.log('change detection');
}
Observable 可以很容易地限制事件,因此我们将创建一个 observable,而不是使用 @HostListener
绑定到调整大小事件:
Observable.fromEvent(window, 'resize')
.throttleTime(200)
.subscribe(_ => this.divWidth = this.parentDiv.nativeElement.clientWidth );
这行得通,但是......在试验时,我发现了一些非常有趣的东西......即使我们限制了调整大小事件,Angular 更改检测仍然 运行s 每次都有是调整大小事件。即,节流不会影响更改检测 运行 的频率。 (托比亚斯博世证实了这一点:
https://github.com/angular/angular/issues/1773#issuecomment-102078250.)
如果事件超过限制时间,我只想将检测更改为 运行。我只需要在此组件上更改检测到 运行。解决方案是在 Angular 区域外创建可观察对象,然后在订阅回调中手动调用更改检测:
constructor(private ngzone: NgZone, private cdref: ChangeDetectorRef) {}
ngAfterViewInit() {
// set initial value, but wait a tick to avoid one-time devMode
// unidirectional-data-flow-violation error
setTimeout(_ => this.divWidth = this.parentDiv.nativeElement.clientWidth);
this.ngzone.runOutsideAngular( () =>
Observable.fromEvent(window, 'resize')
.throttleTime(200)
.subscribe(_ => {
this.divWidth = this.parentDiv.nativeElement.clientWidth;
this.cdref.detectChanges();
})
);
}
这是一个有效的 plunker.
在 plunker 中,我添加了一个 counter
,我使用生命周期钩子 ngDoCheck()
增加了每个变更检测周期。你可以看到这个方法没有被调用——计数器值不会在调整大小事件中改变。
detectChanges()
will run change detection on this component and its children. If you would rather run change detection from the root component (i.e., run a full change detection check) then use ApplicationRef.tick()
而不是(这在 plunker 中被注释掉了)。请注意,tick()
将导致调用 ngDoCheck()
。
这是一个很好的问题。我花了很多时间尝试不同的解决方案,并且学到了很多东西。感谢您提出这个问题。
我用来解决这个问题的其他方法:
import { Component, ChangeDetectorRef } from '@angular/core';
@Component({
selector: 'your-seelctor',
template: 'your-template',
})
export class YourComponent{
constructor(public cdRef:ChangeDetectorRef) { }
ngAfterViewInit() {
this.cdRef.detectChanges();
}
}
只需使用
setTimeout(() => {
//Your expression to change if state
});
最好的解决方案是在服务上使用 setTimeout 或延迟。
Mark Rajcok 给出了很好的答案。更简单的版本(没有限制)是:
ngAfterViewInit(): void {
this.windowResizeSubscription = fromEvent(window, 'resize').subscribe(() => this.onResize())
this.onResize() // to initialize before any change
}
onResize() {
this.width = this.elementRef.nativeElement.getBoundingClientRect().width;
this.changeDetector.detectChanges();
}
我已经阅读并调查了这个错误,但不确定我的情况的正确答案是什么。我知道在开发模式下,更改检测会运行两次,但我不愿意使用 enableProdMode()
来掩盖这个问题。
这是一个简单的示例,其中 table 中的单元格数量应随着 div 宽度的扩展而增加。 (请注意 div 的宽度不仅仅是屏幕宽度的函数,因此 @Media 不能轻易应用)
我的 HTML 看起来如下 (widget.template.html):
<div #widgetParentDiv class="Content">
<p>Sample widget</p>
<table><tr>
<td>Value1</td>
<td *ngIf="widgetParentDiv.clientWidth>350">Value2</td>
<td *ngIf="widgetParentDiv.clientWidth>700">Value3</td>
</tr></table>
这本身没有任何作用。我猜这是因为没有任何东西导致发生变化检测。但是,当我将第一行更改为以下内容并创建一个空函数来接收调用时,它开始工作,但偶尔我会得到 'Expression has changed after it was checked error'
<div #widgetParentDiv class="Content">
gets replaced with
<div #widgetParentDiv (window:resize)=parentResize(10) class="Content">
我最好的猜测是,通过此修改,将触发更改检测并且所有内容都开始响应,但是,当宽度快速变化时会抛出异常,因为上一次更改检测迭代比更改宽度需要更长的时间才能完成div.
- 有没有更好的方法来触发变化检测?
- 我是否应该通过函数捕获调整大小事件以确保 发生变化检测?
- 正在使用#widthParentDiv 访问 div acceptable 的宽度?
- 有没有更好的整体方案?
有关我的项目的更多详细信息,请参阅 this 类似问题。
谢谢
要解决您的问题,您只需在每次调整大小事件后获取 div
的大小并将其存储在组件 属性 中,然后在模板中使用该 属性 .这样,当第 2 轮变化检测 运行s 在开发模式下时,该值将保持不变。
我还建议使用 @HostListener
而不是将 (window:resize)
添加到您的模板。我们将使用 @ViewChild
来获取对 div
的引用。我们将使用生命周期钩子 ngAfterViewInit()
来设置初始值。
import {Component, ViewChild, HostListener} from '@angular/core';
@Component({
selector: 'my-app',
template: `<div #widgetParentDiv class="Content">
<p>Sample widget</p>
<table><tr>
<td>Value1</td>
<td *ngIf="divWidth > 350">Value2</td>
<td *ngIf="divWidth > 700">Value3</td>
</tr>
</table>`,
})
export class AppComponent {
divWidth = 0;
@ViewChild('widgetParentDiv') parentDiv:ElementRef;
@HostListener('window:resize') onResize() {
// guard against resize before view is rendered
if(this.parentDiv) {
this.divWidth = this.parentDiv.nativeElement.clientWidth;
}
}
ngAfterViewInit() {
this.divWidth = this.parentDiv.nativeElement.clientWidth;
}
}
太糟糕了,这不起作用。我们得到
Expression has changed after it was checked. Previous value: 'false'. Current value: 'true'.
错误是抱怨我们的NgIf
表达式——第一次是运行s,divWidth
是0,然后是ngAfterViewInit()
运行s并将值更改为 0 以外的值,然后是第二轮变化检测 运行s(在开发模式下)。值得庆幸的是,有一个 easy/known 解决方案,这是一个一次性问题,而不是像 OP 中那样持续存在的问题:
ngAfterViewInit() {
// wait a tick to avoid one-time devMode
// unidirectional-data-flow-violation error
setTimeout(_ => this.divWidth = this.parentDiv.nativeElement.clientWidth);
}
请注意,此处记录了这种等待一次报价的技术:https://angular.io/docs/ts/latest/cookbook/component-communication.html#!#parent-to-view-child
通常,在 ngAfterViewInit()
和 ngAfterViewChecked()
中,我们需要使用 setTimeout()
技巧,因为这些方法是在组件视图组合后调用的。
这是一个工作 plunker.
我们可以做得更好。我认为我们应该限制调整大小事件,使得 Angular 变化检测仅 运行s,比方说,每 100-250 毫秒,而不是每次发生调整大小事件时。这应该可以防止应用程序在用户调整 window 大小时变得迟钝,因为现在,每个调整大小事件都会导致更改检测到 运行(在开发模式下两次)。您可以通过在之前的 plunker 中添加以下方法来验证这一点:
ngDoCheck() {
console.log('change detection');
}
Observable 可以很容易地限制事件,因此我们将创建一个 observable,而不是使用 @HostListener
绑定到调整大小事件:
Observable.fromEvent(window, 'resize')
.throttleTime(200)
.subscribe(_ => this.divWidth = this.parentDiv.nativeElement.clientWidth );
这行得通,但是......在试验时,我发现了一些非常有趣的东西......即使我们限制了调整大小事件,Angular 更改检测仍然 运行s 每次都有是调整大小事件。即,节流不会影响更改检测 运行 的频率。 (托比亚斯博世证实了这一点: https://github.com/angular/angular/issues/1773#issuecomment-102078250.)
如果事件超过限制时间,我只想将检测更改为 运行。我只需要在此组件上更改检测到 运行。解决方案是在 Angular 区域外创建可观察对象,然后在订阅回调中手动调用更改检测:
constructor(private ngzone: NgZone, private cdref: ChangeDetectorRef) {}
ngAfterViewInit() {
// set initial value, but wait a tick to avoid one-time devMode
// unidirectional-data-flow-violation error
setTimeout(_ => this.divWidth = this.parentDiv.nativeElement.clientWidth);
this.ngzone.runOutsideAngular( () =>
Observable.fromEvent(window, 'resize')
.throttleTime(200)
.subscribe(_ => {
this.divWidth = this.parentDiv.nativeElement.clientWidth;
this.cdref.detectChanges();
})
);
}
这是一个有效的 plunker.
在 plunker 中,我添加了一个 counter
,我使用生命周期钩子 ngDoCheck()
增加了每个变更检测周期。你可以看到这个方法没有被调用——计数器值不会在调整大小事件中改变。
detectChanges()
will run change detection on this component and its children. If you would rather run change detection from the root component (i.e., run a full change detection check) then use ApplicationRef.tick()
而不是(这在 plunker 中被注释掉了)。请注意,tick()
将导致调用 ngDoCheck()
。
这是一个很好的问题。我花了很多时间尝试不同的解决方案,并且学到了很多东西。感谢您提出这个问题。
我用来解决这个问题的其他方法:
import { Component, ChangeDetectorRef } from '@angular/core';
@Component({
selector: 'your-seelctor',
template: 'your-template',
})
export class YourComponent{
constructor(public cdRef:ChangeDetectorRef) { }
ngAfterViewInit() {
this.cdRef.detectChanges();
}
}
只需使用
setTimeout(() => {
//Your expression to change if state
});
最好的解决方案是在服务上使用 setTimeout 或延迟。
Mark Rajcok 给出了很好的答案。更简单的版本(没有限制)是:
ngAfterViewInit(): void {
this.windowResizeSubscription = fromEvent(window, 'resize').subscribe(() => this.onResize())
this.onResize() // to initialize before any change
}
onResize() {
this.width = this.elementRef.nativeElement.getBoundingClientRect().width;
this.changeDetector.detectChanges();
}