42

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

这是一个简单的示例,表格中的单元格数量应随着 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>

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

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

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

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

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

谢谢

4

5 回答 5

68

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

我还建议使用@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;
  }
}

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

检查后表达式已更改。以前的值:“假”。当前值:“真”。

错误是抱怨我们的NgIf表达式——它第一次运行时divWidth为 0,然后ngAfterViewInit()运行并将值更改为 0 以外的值,然后第二轮更改检测运行(在开发模式下)。值得庆幸的是,有一个简单/已知的解决方案,这是一个一次性问题,而不是像 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 更改检测仅运行一次,例如,每 100-250 毫秒,而不是每次发生调整大小事件时。这应该可以防止应用程序在用户调整窗口大小时变得迟缓,因为现在,每个调整大小事件都会导致更改检测运行(在开发模式下两次)。您可以通过将以下方法添加到先前的 plunker 来验证这一点:

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

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

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 区域之外创建 observable,然后在订阅回调中手动调用更改检测:

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()将对此组件及其子组件运行更改检测。如果您希望从根组件运行更改检测(即运行完整的更改检测检查),请ApplicationRef.tick()改用(这在 plunker 中已注释掉)。请注意,这tick()将导致ngDoCheck()被调用。


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

于 2016-08-13T23:21:29.307 回答
17

我用来解决此问题的其他方法:

import { Component, ChangeDetectorRef } from '@angular/core';


@Component({
  selector: 'your-seelctor',
  template: 'your-template',
})

export class YourComponent{

  constructor(public cdRef:ChangeDetectorRef) { }

  ngAfterViewInit() {
    this.cdRef.detectChanges();
  }
}
于 2017-09-21T20:31:17.457 回答
3

只需使用

setTimeout(() => {
  //Your expression to change if state
});
于 2019-02-01T09:08:32.523 回答
0

最好的解决方案是在服务上使用 setTimeout 或延迟。

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

于 2019-07-31T20:35:40.947 回答
0

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();
  }
于 2021-10-28T20:42:29.870 回答