为什么 Angular Form 在 MatDialog 组件中非常慢?

Why is Angular Form very slow within MatDialog component?

我在 Material 对话框组件中有一个 Angular 表单。数据是双向绑定的,当通过输入切换或键入输入时,屏幕会在 keydown 之间锁定几秒钟。所有数据都正确传递,但在尝试使用表单时速度非常慢。

我已经尝试重构表单以用于输入以使用 "Material form" 但仍然具有相同的减速性能。

这是 chrome 中性能跟踪器的屏幕截图:

我的配置有问题吗?或者这是否是最新 Angular 8 个动画/CDK 包中的一个可能的回归?这是我的 Angular 包依赖项:

dependencies": {
    "@angular/animations": "^8.2.13",
    "@angular/cdk": "^8.2.3",
    "@angular/common": "~8.2.13",
    "@angular/compiler": "~8.2.13",
    "@angular/core": "~8.2.13",
    "@angular/forms": "~8.2.13",
    "@angular/material": "^8.2.3",
    "@angular/platform-browser": "~8.2.13",
    "@angular/platform-browser-dynamic": "~8.2.13",
    "@angular/router": "~8.2.13",
}

调用对话框的组件方法如下:

public editRow(tablerow: IRule): void {
const dialogRef = this.dialog.open(EditDialogComponent, {
  width: '100%',
  height: '85%',
  data: tablerow
});

this.subscriptions.push(
  dialogRef.afterClosed().subscribe(updatedRule => {
    if (updatedRule !== undefined) {

      this.rules = this.rules.map(rule => rule.Id === updatedRule.Id ? updatedRule : rule);

      this.subscriptions.push(this.dataService.updateRule(updatedRule).subscribe(
        response => {
          this.snackBar.openFromComponent(SuccessComponent, {
            duration: 3000,
            data: `Rule added`
          });
        }, error => {
          this.snackBar.openFromComponent(ErrorComponent, {
            duration: 10000,
            data: 'Internal Server Error'
          });
          }
        ));
      }
    })
  );
}

包含表单的 mat 对话框模板:

<mat-dialog-content>
<i id="close-icon" class="material-icons md-24" aria-label="close"
[mat-dialog-close]>close</i>
<div class="brand-panel-container">
<div class="brand-panel">
  <div class="brand-panel-header">
    <div class="brand-title">
      <h4 mat-dialog-title>Rule: {{ data.Id }}</h4>
    </div>
  </div>
  <form #ruleForm="ngForm">
    <div class="row">
      <div class="col-md-6">
        <div class="form-group">
          <label for="name">Shop Type:<span class="asterisk">*</span></label>
          <select
            [(ngModel)]="data.Type.Text"
            value="{{ data.Type.Text }}"
            name="type"
            type="text"
            class="form-control"
            id="type"
            required>
            <option *ngFor="let opt of shopTypeOpts; trackBy: indentify" value="{{opt.Text}}">{{opt.Text}}</option>
          </select>
        </div>
      </div>
      <div class="col-md-6">
        <div class="form-group">
          <label for="name">Origin:<span class="asterisk">*</span></label>
          <input
            [(ngModel)]="data.Origin"
            value="{{ data.Origin }}"
            name="origin"
            type="text"
            class="form-control"
            id="origin"
            required>
        </div>
      </div>
    </div>

    <div class="row">
      <div class="col-md-6">
        <div class="form-group">
          <label for="name">Destination:<span class="asterisk">*</span></label>
          <input
            [(ngModel)]="data.Destination"
            value="{{ data.Destination }}"
            name="destination"
            type="text"
            class="form-control"
            id="destination"
            required>
        </div>
      </div>
      <div class="col-md-6">
        <div class="form-group">
          <label for="name">Fare:<span class="asterisk">*</span></label>
          <input
            [(ngModel)]="data.Fare.Text"
            value="{{ data.Fare.Text }}"
            name="fare"
            type="text"
            class="form-control"
            id="fare"
            required>
        </div>
      </div>
    </div>

    <div class="row">
      <div class="col-md-6">
        <div class="form-group">
          <label for="name">Government:<span class="asterisk">*</span></label>
          <select
            [(ngModel)]="data.Government.Text"
            value="{{ data.Government.Text }}"
            name="government"
            type="text"
            class="form-control"
            id="government"
            required>
            <option *ngFor="let opt of governmentTypeOpts; trackBy: indentify"
              value="{{opt.Text}}">{{opt.Text}}</option>
          </select>
        </div>
      </div>
      <div class="col-md-6">
        <div class="form-group">
          <label for="name">Special Pricing:<span class="asterisk">*</span></label>
          <select
            [(ngModel)]="data.SpecialPricing.Text"
            value="{{ data.SpecialPricing.Text }}"
            name="specialPricing"
            type="text"
            class="form-control"
            id="specialPricing"
            required>
            <option *ngFor="let opt of specialPricingTypeOpts; trackBy: indentify"
              value="{{opt.Text}}">{{opt.Text}}</option>
          </select>
        </div>
      </div>
    </div>

    <div class="row">
      <div class="col-md-6">
        <div class="form-group">
          <label for="name">Upgrade:<span class="asterisk">*</span></label>
          <select
            [(ngModel)]="data.Upgrade.Text"
            value="{{ data.Upgrade.Text }}"
            name="upgrade"
            type="text"
            class="form-control"
            id="upgrade"
            required>
            <option *ngFor="let opt of upgradeTypeOpts; trackBy: indentify"
              value="{{opt.Text}}">{{opt.Text}}</option>
          </select>
        </div>
      </div>
      <div class="col-md-6">
        <div class="form-group">
          <label for="name">Cabin Count:<span class="asterisk">*</span></label>
          <input
            [(ngModel)]="data.CabinCount"
            value="{{ data.CabinCount }}"
            name="cabinCount"
            type="text"
            class="form-control"
            id="cabinCount"
            required>
        </div>
      </div>
    </div>

    <div class="row">
      <div class="col-md-6">
        <div class="form-group">
          <label for="name">Columns Count:<span class="asterisk">*</span></label>
          <input
            [(ngModel)]="data.ColumnsCount"
            value="{{ data.ColumnsCount }}"
            name="columnsCount"
            type="text"
            class="form-control"
            id="columnsCount"
            required>
        </div>
      </div>
      <div class="col-md-6">
        <div class="form-group">
          <label for="name">Lang Code:<span class="asterisk">*</span></label>
          <input
            [(ngModel)]="data.LangCode"
            value="{{ data.LangCode }}"
            name="langCode"
            type="text"
            class="form-control"
            id="langCode"
            required>
        </div>
      </div>
    </div>

    <div class="row">
      <div class="col-md-6">
        <div class="form-group">
          <label for="name">Fare Wheel Search?:<span class="asterisk">*</span></label>
          <input
            [(ngModel)]="data.IsFareWheelSearch"
            value="{{ data.IsFareWheelSearch }}"
            name="isFareWheelSearch"
            type="text"
            class="form-control"
            id="isFareWheelSearch"
            required>
        </div>
      </div>
      <div class="col-md-6">
        <div class="form-group">
          <label for="name">Markets:<span class="asterisk">*</span></label>
          <select
            [(ngModel)]="data.Markets.Text"
            value="{{ data.Markets.Text }}"
            name="markets"
            type="text"
            class="form-control"
            id="markets"
            required>
            <option *ngFor="let opt of marketTypeOpts; trackBy: indentify" value="{{opt.Text}}">{{opt.Text}}</option>
          </select>
        </div>
      </div>
    </div>

    <div class="row">
      <div class="col-md-6">
        <div class="form-group">
          <label for="name">POS:<span class="asterisk">*</span></label>
          <input
            [(ngModel)]="data.POS"
            value="{{ data.POS }}"
            name="pos"
            type="text"
            class="form-control"
            id="pos"
            required>
        </div>
      </div>
      <div class="col-md-6">
        <div class="form-group">
          <label for="name">Columns:<span class="asterisk">*</span></label>
          <input
            [(ngModel)]="data.Columns"
            value="{{ data.Columns }}"
            name="columns"
            type="text"
            class="form-control"
            id="columns"
            required>
        </div>
      </div>
    </div>

    <div mat-dialog-actions>
      <span *ngIf="!ruleForm.valid" class="invalid-msg"><span
          class="asterisk">*</span>All fields must be filled in to save
        changes.</span>
      <button mat-button class="brand-default-button"
        [mat-dialog-close]>Cancel</button>
      <button mat-button class="brand-confirm-button" type="submit"
        [disabled]="!ruleForm.valid" [mat-dialog-close]="data.Id"
        (click)="onSaveData(ruleForm.value)">Save Changes</button>
    </div>
  </form>
</div>

对话框组件文件:

import { Component, Inject } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { IRule } from '../../../models/rule.interface';
import { OptionsService } from 'src/app/shared/services/options.service';
import { IDropdownOption } from 'src/models/dropdown-option.interface';

@Component({
  selector: 'app-edit-dialog',
  templateUrl: './edit-dialog.component.html',
  styleUrls: ['./edit-dialog.component.scss']
})
export class EditDialogComponent {

  public shopTypeOpts: IDropdownOption[] = [];
  public governmentTypeOpts: IDropdownOption[] = [];
  public specialPricingTypeOpts: IDropdownOption[] = [];
  public fareTypeOpts: IDropdownOption[] = [];
  public upgradeTypeOpts: IDropdownOption[] = [];
  public marketTypeOpts: IDropdownOption[] = [];

  constructor(
    public dialogRef: MatDialogRef<EditDialogComponent>,
    public optionsService: OptionsService,
    @Inject(MAT_DIALOG_DATA) public data: IRule) {
    this.shopTypeOpts = this.optionsService.shopTypeOptions;
    this.governmentTypeOpts = this.optionsService.governmentTypeOptions;
    this.specialPricingTypeOpts = this.optionsService.specialPricingTypeOptions;
    this.fareTypeOpts = this.optionsService.fareTypeOptions;
    this.upgradeTypeOpts = this.optionsService.upgradeTypeOptions;
    this.marketTypeOpts = this.optionsService.marketTypeOptions;
  }

  public onSaveData(updatedRule: IRule): void {
    this.dialogRef.close(updatedRule);
  }

  public indentify(index, item) {
    return item.Text;
  }

}

IDropdownOption 界面:

export interface IDropdownOption {
  Text: string;
  Value: number;
}

*已编辑以包含 trackBy 函数和 IDropdownOption 界面以查看唯一标识符。 *

速度变慢似乎是因为下拉选项被反复循环...也许需要更改 changeDetection 策略?

我会把它作为一个答案,因为我想我知道是什么导致了你的速度变慢。除了 *ngFor 缺少 trackBy 方法外,它还必须在您输入的每个内容中检查整个模板。建议使用 OnPush 变更检测策略。这可能会使某些事情无法按照您期望的方式工作,但这是使组件快速运行的一种非常好的方法,因为只有在更改输入后才会检查更改(以及其他事情。

关于这个主题,article 还是不错的。

您应该更改对话框组件装饰器以包含 OnPush:

@Component({
  selector: 'app-edit-dialog',
  templateUrl: './edit-dialog.component.html',
  styleUrls: ['./edit-dialog.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})

非常感谢大家的帮助。我通过在父组件中使用 ChangeDetectorRef 在对话框打开后分离并在对话框关闭后重新附加来解决我的问题。这可以防止 EditDialogComponent 的任何重新渲染/重新绘制并修复性能问题。

public editRow(tablerow: IRule): void {

this.changeDetectorRef.detach(); // Detach change detection before the dialog opens. 

const dialogRef = this.dialog.open(EditDialogComponent, {
  width: '100%',
  height: '85%',
  data: tablerow
});

this.subscriptions.push(
  dialogRef.afterClosed().subscribe(updatedRule => {

    this.changeDetectorRef.reattach(); // Reattach change detection after the dialog closes.

    if (updatedRule !== undefined) {

      this.rules = this.rules.map(rule => rule.Id === updatedRule.Id ? updatedRule : rule);

      this.subscriptions.push(this.dataService.updateRule(updatedRule).subscribe(
        response => {
          this.snackBar.openFromComponent(SuccessComponent, {
            duration: 3000,
            data: `Rule added`
          });
          }, error => {
            this.snackBar.openFromComponent(ErrorComponent, {
              duration: 10000,
              data: 'Internal Server Error'
            });
          }
        ));
      }
    })
  );
}

此答案取决于用例。 听起来可能很奇怪!

背景: 我已经在我的自定义 grid/table 组件中实现了 OnPush 策略以及分离重新附加 ChangeDetectorRef,其中很少有元素在 renderer2[ 的帮助下使用指令添加了玻璃覆盖层

我在打开对话框时添加了模糊背景 'backdropClass' 属性。

当屏幕尺寸更大时会导致延迟,如果我减小屏幕尺寸它表现良好。如果我从 Dom 树中删除一些元素,它会表现良好。

所以我发现是背景 class 应用缓慢,导致整个对话框出现滞后的错觉。

只需删除它。

希望这可以帮助那些正在为所有已实施的解决方案抓耳挠腮的人:)

 this.dialog
  .open(CreateKeyComponent, {
    disableClose: true,
    // backdropClass: 'blur',  -----> root cause
    position: { top: '15vh' },
    data: {
      data: this.tableDataShowFlag ? '1' : '0',
    },
  })