NGZORRO:一种Angular页面性能优化手段

58 篇文章 3 订阅

 事情是这样的,今天在看NGZORRO时候看见这么一句话。

OnPush模式,性能卓越。🤔 那我就来看看ant desgin是怎么做出性能优化的。

 Angular有两种变化检测策略(Change Detection Strategy)

enum ChangeDetectionStrategy {
  // 使用 CheckOnce 策略,这意味着自动更改检测将停用,直到通过将策略设置为"Default"(总是检查)重新激活。
  OnPush: 0 

  // 使用默认的 CheckAlway 策略,其中更改检测是自动的,直到显式停用
  Default: 1
}

在一般情况下,Angular组件的默认变化检测策略默认为Default。

import { Component, OnInit } from "@angular/core";

@Component({
  selector: "app-timer",
  template: `
    <div>Counter-timer: {{ timer }}</div>
  `
})
export class TimerComponent implements OnInit {
  timer: number = 0;

  ngOnInit() {
    setInterval(() => {
      this.timer++;
      console.log(this.timer);
    }, 1000);
  }
}

父组件引用计时组件TimerComponent

import { Component } from "@angular/core";

@Component({
  selector: "my-app",
  template: `
    <app-timer></app-timer>
  `
})
export class AppComponent {}

可以看到页面上的计时在不断的跳动,这个timer组件的计时数据完全由自身的setInterval触发,而Angular的变化检测会不停的从父组件开始检测,并向下检测子组件的数据变化,一但检测到有数据变化,立即渲染页面。这种默认的变化检测策略使用起来很方便,无需多余的代码,但如果组件众多,组件中又包含组件,那每次这种不加区分的变化检测对页面的性能却有一定的消耗,而针对这种问题优化就有了onPush的变化检测策略。

使用变化检测策略OnPush是Angular常见的页面性能优化手段。

@Component({
  // ...
  changeDetection: ChangeDetectionStrategy.OnPush //设置变化检测策略为OnPush
}
export class TimerComponent {
  // ...
}

当组件使用OnPush变化检测策略时,只有在以下任一情况下,Angular才会检测此组件
1.组件的任一@Input属性被父组件设为新值,如果@Input是引用类型,则需被设为新的实例才会触发变化检测。
2.事件发生。

当TimerComponent使用OnPush变化检测策略后,再看AppComponent页面时,计时器不再跳动,始终是0,但在控制台中,你可以看到timer的值每秒其实是在变化的,只是页面没有渲染而已。

这时如果我们给TimerComponent组件加上一个无关紧要的输入参数x,这个x不参与任何计时逻辑,代码如下
 

import {
  ChangeDetectionStrategy,
  Component,
  Input,
  OnInit
} from "@angular/core";

@Component({
  selector: "app-timer",
  template: `
    <div>Counter-timer: {{ timer }}</div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TimerComponent implements OnInit {
  timer: number = 0;
  @Input() x: number = 0; // 输入参数x为数值型,并且不参与任何业务

  ngOnInit() {
    setInterval(() => {
      this.timer++;
      console.log(this.timer);
    }, 1000);
  }
}

我们在AppComponent中添加一个按钮用来修改i值,并将i传入到子组件TimerComponent中,代码如下

import { Component } from "@angular/core";

@Component({
  selector: "my-app",
  template: `
    <app-timer [x]="i"></app-timer>
    <button (click)="addi()">Add i</button>
  `
})
export class AppComponent {
  i: number = 0;

  addi() {
    this.i++;
  }
}

运行代码,查看AppComponent页面,你可以发现页面上计时器不跳动,但当你点击Add i按钮后,计时器就会刷新数字。这是因为当子组件使用OnPush变化检测策略时,子组件的任一@Input属性被父组件设为新值时,无论这个@Input属性是否影响到当前页面,Angular都会对此组件进行一次变化检测。

我们再试一下@Input属性传入的是引用型变量,计时子组件代码如下
 

import {
  ChangeDetectionStrategy,
  Component,
  Input,
  OnInit
} from "@angular/core";
import { DataModel } from "../app.component";

@Component({
  selector: "app-timer",
  template: `
    <div>Counter-timer: {{ timer }}</div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TimerComponent implements OnInit {
  timer: number = 0;
  @Input() x: DataModel = new DataModel(); // 输入参数改为引用型

  ngOnInit() {
    setInterval(() => {
      this.timer++;
      console.log(this.timer);
    }, 1000);
  }
}

AppComponent父组件代码如下

import { Component } from "@angular/core";

@Component({
  selector: "my-app",
  template: `
    <app-timer [x]="data"></app-timer>
    <button (click)="addi()">Add i</button>
  `
})
export class AppComponent {
  data: DataModel = new DataModel();

  addi() {
    this.data.i++;
  }
}

export class DataModel {
  i: number = 0;
}

运行代码后,你可以发现页面上计时器不跳动,但当你点击Add i按钮后,计时器仍旧不跳动。这是因为只改变引用类型里的某个值,Angular认为@Input传入值还是同样的引用地址,算没有设为新值,从而没有触发变化检测。如需触发检测,则需要传入不同的引用地址,也就是要传一个全新的引用型变量,此例,只要将AppComponent父组件代码增加一句话就行
 

import { Component } from "@angular/core";

@Component({
  selector: "my-app",
  template: `
    <app-timer [x]="data"></app-timer>
    <button (click)="addi()">Add i</button>
  `
})
export class AppComponent {
  data: DataModel = new DataModel();

  addi() {
    this.data.i++;
    this.data = { i: this.data.i }; // 新增的语句,将data变成新的实例
  }
}

export class DataModel {
  i: number = 0;
}

注:我个人在写组件的过程中,出现过这么一个情况,需要对传入的数组进行解构赋值才可以更新组件,是一样的道理。

与Angular配合的UI框架NG-ZORRO从 7.0 版本开始,NG-ZORRO 组件默认在 OnPush 模式下工作,当你给NG-ZORRO组件传入对象时那就要十分注意上述问题。

如果在子组件中触发一个事件,无论此事件是否和业务有关,甚至是个空事件,那在onPush策略下的子组件也会进行变换检测。代码如下


TimerComponent

import {
  ChangeDetectionStrategy,
  Component,
  Input,
  OnInit
} from "@angular/core";
import { DataModel } from "../app.component";

@Component({
  selector: "app-timer",
  template: `
    <div>Counter-timer: {{ timer }}</div>
    <button (click)="addi()">Add i on timer</button>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TimerComponent implements OnInit {
  timer: number = 0;

  addi() {} // 空事件也能触发此组件的变化检测

  ngOnInit() {
    setInterval(() => {
      this.timer++;
      console.log(this.timer);
    }, 1000);
  }
}

AppComponent

import { Component } from "@angular/core";

@Component({
  selector: "my-app",
  template: `
    <app-timer></app-timer>
  `
})
export class AppComponent {}

代码运行的效果就是,页面上计时器不跳动,但当你点击Add i on timer按钮后,计时器会跳动一下。

ChangeDetectorRef类

ChangeDetectorRef是组件的变化检测器的引用,我们可以通过依赖注入的方式来获取该实例

...
import { ChangeDetectorRef } from "@angular/core";

@Component({...})
export class TimerComponent implements OnInit {
  ...
  constructor(private cdRef: ChangeDetectorRef) {}
  ...
}

ChangeDetectorRef类包含5个方法

abstract class ChangeDetectorRef {
  abstract markForCheck(): void //当组件使用OnPush变更检测策略时,把该组件显式标记为已更改,以便它再次进行检查。
  abstract detach(): void //从变更检测树中分离开组件。 已分离的组件在重新附加上去之前不会被检查。 与 detectChanges() 结合使用,可以实现局部变更检测。
  abstract detectChanges(): void //检查该组件及其子组件。与 detach 结合使用可以实现局部变更检测。
  abstract checkNoChanges(): void //检查变更检测器及其子检测器,如果检测到任何更改,则抛出异常。
  abstract reattach(): void //把以前分离开的组件重新附加到变更检测树上。 组件会被默认附加到这棵树上。
}

我们着重说明一下markForCheck()方法,当组件使用OnPush变更检测策略时,遇到markForCheck(),则把此组件标记为已更改,已遍Angular的变化检测能在此组件没有@Input属性更新或事件发生的情况下,也能检测到此组件的变化。见以下代码

TimerComponent

import {
  ChangeDetectionStrategy,
  ChangeDetectorRef,
  Component,
  OnInit
} from "@angular/core";

@Component({
  selector: "app-timer",
  template: `
    <div>Counter-timer: {{ timer }}</div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TimerComponent implements OnInit {
  timer: number = 0;

  constructor(private cdRef: ChangeDetectorRef) {}

  ngOnInit() {
    setInterval(() => {
      this.timer++;

	  // 到第三秒时,标记此组件已变化
      if (this.timer == 3) {
        this.cdRef.markForCheck();
      }
      console.log(this.timer);
    }, 1000);
  }
}

AppComponent

import { Component } from "@angular/core";

@Component({
  selector: "my-app",
  template: `
    <app-timer></app-timer>
  `
})
export class AppComponent {}

运行代码,你可以看到计时器一开始不跳动,到第三秒的时候,计时器数字跳为3,然后就一直不再跳动。这是因为到第三秒的时候,运行了“this.cdRef.markForCheck();”这句话,标记了TimerComponet已经变化,Angular检测到变化标记后刷新页面数据,并将TimerComponet标记为无变化。所以“this.cdRef.markForCheck();”这句话只能引起一次变化检测,如要多次,则要多次的运行这句话,如果以上代码功能要改成3秒后开始计时,只要将this.timer == 3改成this.timer >= 3就行了。
 

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

董厂长

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值