详解Angular中的变更检测(十三)- 减少变更检测次数

91 阅读2分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第11天,点击查看活动详情

变更检测的性能

默认情况下,当我们的组件中某个值发生了变化触发了变更检测,那么Angular会从根组件上往下检查所有的组件。不过Angular对每个组件进行变更检测的速度非常快。 尽管 Angular 进行了大量优化,但是遇到了大型应用,变化检测的性能仍然会下降,所以为了提升性能,就要减少变更检测的次数。

减少变更检测次数

我们可以使用 OnPush的策略来优化我们的应用,那么这就够了吗? 在我们实际的开发中还会有很多的场景,我们需要通过一些其他的方式来继续优化我们的应用。

场景一

@Component({
  selector: "app-enter",
  template: `<input #input type="text" />`,
})
export class EnterComponent implements AfterViewInit {
  @ViewChild("input", { read: ElementRef })
  private inputElementRef: any;

  constructor() {}

  ngAfterViewInit(): void {
    this.inputElementRef.nativeElement.addEventListener(
      "keydown",
      (event: KeyboardEvent) => {
        const keyCode = event.which || event.keyCode;
        if (keyCode === 13) {
          this.search();
        }
      }
    );
  }

  search() {
    // ...
  }
}

事件会触发Angular的变更检测,在示例中绑定 keydown 事件后,每一次键盘输入都会触发变更检测,而这些变更检测大多数都是多余的检测,只有当按键为 Enter 时,才需要真正的进行变更检测。

上面的例子,如果不想让事件触发变更检测,怎么实现呢?我们就可以利用 NgZone.runOutsideAngular() 来减少变更检测的次数。

基于上面的例子,我们用指令封装一下。

@Directive({
    selector: '[enter]'
})
export class ThyEnterDirective implements OnInit {
    @Output() enter = new EventEmitter();

    constructor(private ngZone: NgZone, private elementRef: ElementRef<HTMLElement>) {}

    ngOnInit(): void {
        // 包裹代码将运行在Zone区域之外
        this.ngZone.runOutsideAngular(() => {
            this.elementRef.nativeElement.addEventListener('keydown', (event: KeyboardEvent) => {
                const keyCode = event.which || event.keyCode;
                if (keyCode === 13) {
                    this.ngZone.run(() => {
                        this.enter.emit(event);
                    });
                }
            });
        });
    }
}

场景二

假如我们使用 WebSocket 将大量数据从后端推送到前端,则相应的前端组件应仅每 10 秒更新一次。在这种情况下,我们可以通过调用detach() 和手动触发它来停用变更检测。 这里暂时模拟一下,首先要detach()一下,变更检测就跳过了,然后在需要执行变更检测的地方手动调用变更检测就可以了。

@Component({
  selector: 'main',
  template: `
  <h1>{{count}}</h1>
  `
})
export class MainComponent {
  @Input() actionSubject$!: Subject<any>;
  @Input() options: any;

  count: number = 0;

 constructor(private cdr: ChangeDetectorRef) {}
 ngOnInit() {}
    cdr.detach(); // 停用变化检测

    setInterval(() => {
      this.cdr.detectChanges(); // 手动触发变化检测
    }, 10 * 1000);
  }

问题: 当页面完成加载后,页面上显示什么?

当然使用 ****ngZone.runOutsideAngular() 也可以处理这种场景。

实际上,上面的两个例子是相通的。

总结

为了提升性能,我们就需要尽量的去减少变更检测的次数,除了之前说的可以使用OnPush策略来减少变更检测次数,还可以采用本文中的一些方法。