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.

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

有关我的项目的更多详细信息,请参阅 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 或延迟。

https://blog.angular-university.io/angular-debugging/

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