Angular2 - 表达式在检查后发生了变化 - 绑定到具有调整大小事件的 div 宽度 [英] Angular2 - Expression has changed after it was checked - Binding to div width with resize events

查看:15
本文介绍了Angular2 - 表达式在检查后发生了变化 - 绑定到具有调整大小事件的 div 宽度的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我已经对此错误进行了一些阅读和调查,但不确定适合我的情况的正确答案是什么.我知道在开发模式下,更改检测会运行两次,但我不愿意使用 enableProdMode() 来掩盖问题.

这是一个简单的例子,表格中的单元格数量应该随着 div 的宽度扩大而增加.(注意div的宽度不仅仅是屏幕宽度的函数,所以@Media不能轻易应用)

我的 HTML 如下所示 (widget.template.html):

<p>示例小部件</p><表格><tr><td>值1</td><td *ngIf="widgetParentDiv.clientWidth>350">Value2</td><td *ngIf="widgetParentDiv.clientWidth>700">Value3</td></tr></table>

这本身没有任何作用.我猜这是因为没有什么会导致发生变化检测.但是,当我将第一行更改为以下内容并创建一个空函数来接收调用时,它开始工作,但偶尔我会收到检查后表达式已更改错误"

被替换为<div #widgetParentDiv (window:resize)=parentResize(10) class="Content">

我最好的猜测是,通过此修改,触发了更改检测并且一切都开始响应,但是,当宽度快速更改时,会抛出异常,因为之前的更改检测迭代需要比更改 div 的宽度更长的时间才能完成.

  1. 是否有更好的方法来触发变更检测?
  2. 我是否应该通过函数捕获调整大小事件以确保发生变化检测?
  3. 正在使用 #widthParentDiv 访问div的宽度可以接受吗?
  4. 是否有更好的整体解决方案?

有关我的项目的更多详细信息,请参阅这个类似问题.

谢谢

解决方案

要解决您的问题,您只需在每次调整大小事件后获取并存储 div 的大小到组件属性中,并在模板中使用该属性.这样,当第二轮变化检测在开发模式下运行时,该值将保持不变.

我还建议使用 @HostListener 而不是将 (window:resize) 添加到您的模板中.我们将使用 @ViewChild 来获取对 div 的引用.我们将使用生命周期钩子 ngAfterViewInit() 来设置初始值.

import {Component, ViewChild, HostListener} from '@angular/core';@零件({选择器:'我的应用',模板:`<div #widgetParentDiv class="Content"><p>示例小部件</p><表格><tr><td>值1</td><td *ngIf="divWidth > 350">Value2</td><td *ngIf="divWidth > 700">Value3</td></tr></table>`,})导出类 AppComponent {divWidth = 0;@ViewChild('widgetParentDiv') parentDiv:ElementRef;@HostListener('window:resize') onResize() {//在渲染视图之前防止调整大小如果(this.parentDiv){this.divWidth = this.parentDiv.nativeElement.clientWidth;}}ngAfterViewInit() {this.divWidth = this.parentDiv.nativeElement.clientWidth;}}

太糟糕了,这不起作用.我们得到

<块引用>

表达式在检查后发生了变化.以前的值:'假'.当前值:'真'.

错误是抱怨我们的 NgIf 表达式——它第一次运行时,divWidth 是 0,然后 ngAfterViewInit() 运行并将值更改为 0 以外的值,然后运行第二轮更改检测(在开发模式下).谢天谢地,有一个简单/已知的解决方案,这是一个一次性的问题,而不是像 OP 那样的持续问题:

 ngAfterViewInit() {//等待一个滴答声以避免一次性 devMode//单向数据流违规错误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 更改检测只运行,比如每 100-250 毫秒,而不是每次发生调整大小事件时.这应该可以防止应用程序在用户调整窗口大小时变得缓慢,因为现在,每个调整大小事件都会导致运行更改检测(在开发模式下两次).您可以通过在之前的plu​​nker中添加以下方法来验证这一点:

ngDoCheck() {console.log('变化检测');}

Observable 可以轻松地限制事件,因此我们将创建一个 observable,而不是使用 @HostListener 绑定到调整大小事件:

Observable.fromEvent(window, 'resize').throttleTime(200).subscribe(_ => this.divWidth = this.parentDiv.nativeElement.clientWidth );

这是可行的,但是...在试验时,我发现了一些非常有趣的东西...即使我们限制了调整大小事件,每次发生调整大小事件时,Angular 更改检测仍然运行.即,限制不会影响更改检测运行的频率.(Tobias Bosch 证实了这一点:https://github.com/angular/angular/issues/1773#issuecomment-102078250.)

我只希望在事件超过节流时间时运行更改检测.我只需要更改检测即可在此组件上运行.解决办法是在Angular zone外创建observable,然后在订阅回调内手动调用change detection:

constructor(private ngzone: NgZone, private cdref: ChangeDetectorRef) {}ngAfterViewInit() {//设置初始值,但等待一个刻度以避免一次性 devMode//单向数据流违规错误setTimeout(_ => this.divWidth = this.parentDiv.nativeElement.clientWidth);this.ngzone.runOutsideAngular(() =>Observable.fromEvent(window, 'resize').throttleTime(200).订阅(_ => {this.divWidth = this.parentDiv.nativeElement.clientWidth;this.cdref.detectChanges();}));}

这是一个有效的 plunker.

在 plunker 中,我添加了一个 counter,我使用生命周期钩子 ngDoCheck() 递增每个更改检测周期.你可以看到这个方法没有被调用 –计数器值在调整大小事件时不会改变.

detectChanges() 将对该组件及其子组件运行更改检测.如果您更愿意从根组件运行更改检测(即运行完整的更改检测检查),请使用 ApplicationRef.tick() (这在 plunker 中被注释掉了).请注意,tick() 将导致 ngDoCheck() 被调用.

<小时>

这是一个很好的问题.我花了很多时间尝试不同的解决方案,我学到了很多.感谢您发布这个问题.

I have done some reading and investigation on this error, but not sure what the correct answer is for my situation. I understand that in dev mode, change detection runs twice, but I am reluctant to use enableProdMode() to mask the issue.

Here is a simple example where the number of cells in the table should increase as the width of the div expands. (Note that the width of the div is not a function of just the screen width, so @Media cannot easily be applied)

My HTML looks as follows (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>

This on its own does nothing. I'm guessing this is because nothing is causing change detection to occur. However, when I change the first line to the following, and create an empty function to receive the call, it starts working, but occasionally I get the 'Expression has changed after it was checked error'

<div #widgetParentDiv class="Content">
   gets replaced with
      <div #widgetParentDiv (window:resize)=parentResize(10) class="Content">

My best guess is that with this modification, change detection is triggered and everything starts responding, however, when the width changes rapidly the exception is thrown because the previous iteration of change detection took longer to complete than changing the width of the div.

  1. Is there a better approach to triggering the change detection?
  2. Should I be capturing the resize event through a function to ensure change detection occurs?
  3. Is using #widthParentDiv to access the width of the div acceptable?
  4. Is there a better overall solution?

For more details on my project please see this similar question.

Thanks

解决方案

To solve your issue, you simply need to get and store the size of the div in a component property after each resize event, and use that property in the template. This way, the value will stay constant when the 2nd round of change detection runs in dev mode.

I also recommend using @HostListener rather than adding (window:resize) to your template. We'll use @ViewChild to get a reference to the div. And we'll use lifecycle hook ngAfterViewInit() to set the initial value.

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;
  }
}

Too bad that doesn't work. We get

Expression has changed after it was checked. Previous value: 'false'. Current value: 'true'.

The error is complaining about our NgIf expressions -- the first time it runs, divWidth is 0, then ngAfterViewInit() runs and changes the value to something other than 0, then the 2nd round of change detection runs (in dev mode). Thankfully, there is an easy/known solution, and this is a one-time only issue, not a continuing issue like in the OP:

  ngAfterViewInit() {
    // wait a tick to avoid one-time devMode
    // unidirectional-data-flow-violation error
    setTimeout(_ => this.divWidth = this.parentDiv.nativeElement.clientWidth);
  }

Note that this technique, of waiting one tick is documented here: https://angular.io/docs/ts/latest/cookbook/component-communication.html#!#parent-to-view-child

Often, in ngAfterViewInit() and ngAfterViewChecked() we'll need to employ the setTimeout() trick because these methods are called after the component's view is composed.

Here's a working plunker.


We can make this better. I think we should throttle the resize events such that Angular change detection only runs, say, every 100-250ms, rather then every time a resize event occurs. This should prevent the app from getting sluggish when the user is resizing the window, because right now, every resize event causes change detection to run (twice in dev mode). You can verify this by adding the following method to the previous plunker:

ngDoCheck() {
   console.log('change detection');
}

Observables can easily throttle events, so instead of using @HostListener to bind to the resize event, we'll create an observable:

Observable.fromEvent(window, 'resize')
   .throttleTime(200)
   .subscribe(_ => this.divWidth = this.parentDiv.nativeElement.clientWidth );

This works, but... while experimenting with that, I discovered something very interesting... even though we throttle the resize event, Angular change detection still runs every time there is a resize event. I.e., the throttling does not affect how often change detection runs. (Tobias Bosch confirmed this: https://github.com/angular/angular/issues/1773#issuecomment-102078250.)

I only want change detection to run if the event passes the throttle time. And I only need change detection to run on this component. The solution is to create the observable outside the Angular zone, then manually call change detection inside the subscription callback:

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();
       })
  );
}

Here's a working plunker.

In the plunker I added a counter that I increment every change detection cycle using lifecycle hook ngDoCheck(). You can see that this method is not being called – the counter value does not change on resize events.

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() instead (this is commented out in the plunker). Note that tick() will cause ngDoCheck() to be called.


This is a great question. I spent a lot of time trying out different solutions and I learned a lot. Thank you for posting this question.

这篇关于Angular2 - 表达式在检查后发生了变化 - 绑定到具有调整大小事件的 div 宽度的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆