Angular2 - 表达式在检查后发生了变化 - 使用resize事件绑定到div宽度

Ant*_*Day 40 javascript responsive-design angular2-changedetection angular

我已经对这个错误进行了一些阅读和调查,但不确定我的情况的正确答案是什么.我知道在开发模式下,更改检测运行两次,但我不愿意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>
Run Code Online (Sandbox Code Playgroud)

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

<div #widgetParentDiv class="Content">
   gets replaced with
      <div #widgetParentDiv (window:resize)=parentResize(10) class="Content">
Run Code Online (Sandbox Code Playgroud)

我最好的猜测是,通过这种修改,触发更改检测并且一切都开始响应,但是,当宽度快速变化时抛出异常,因为前一次更改检测迭代需要更长的时间来完成,而不是更改div的宽度.

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

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

谢谢

Mar*_*cok 65

要解决您的问题,您只需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;
  }
}
Run Code Online (Sandbox Code Playgroud)

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

检查后表情发生了变化.上一个值:'false'.当前值:'true'.

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

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

请注意,这种等待一个滴答的技术在此处记录:https://angular.io/docs/ts/latest/cookbook/component-communication.html#!# parent-to-view- child

通常情况下,在ngAfterViewInit()ngAfterViewChecked()我们需要使用的setTimeout()伎俩,因为组件的视图是由后被调用这些方法.

这是一个工作plunker.


我们可以做得更好.我认为我们应该限制调整大小事件,以便Angular变化检测仅运行,例如,每100-250ms运行一次,而不是每次调整resize事件.这可以防止应用程序在用户调整窗口大小时变得迟缓,因为现在,每个调整大小事件都会导致更改检测运行(在开发模式下两次).您可以通过将以下方法添加到上一个plunker来验证这一点:

ngDoCheck() {
   console.log('change detection');
}
Run Code Online (Sandbox Code Playgroud)

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

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

这很有效,但是......在尝试这个时,我发现了一些非常有趣的东西......即使我们调整了resize事件,每当有一个resize事件时,Angular变化检测仍会运行.即,限制不会影响更改检测的运行频率.(Tobias Bosch证实了这一点: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();
       })
  );
}
Run Code Online (Sandbox Code Playgroud)

这是一个工作plunker.

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

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


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

  • @MarkRajcok因为我们订阅了Angular以外的可观察对象.我们应该在组件被销毁时明确取消订阅以防止内存泄漏./sf/answers/2638819501/ (2认同)

小智 15

我用来解决这个问题的其他方式:

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


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

export class YourComponent{

  constructor(public cdRef:ChangeDetectorRef) { }

  ngAfterViewInit() {
    this.cdRef.detectChanges();
  }
}
Run Code Online (Sandbox Code Playgroud)