1. 程式人生 > >Angular效能優化之髒檢測

Angular效能優化之髒檢測

Angular效能優化之髒檢測

當我們在使用 Angular 框架搭建專案時,隨著元件越來越多,頁面也來越複雜,效能會越來越低,主要表現在 CPU 使用率 很高。所以我們要對專案做一定的優化。

Angular髒檢查(Change Detection)機制

Angular 的髒檢測主要是指 zone.js,這是一個開源的第三方庫,github地址

關於 zone.js 的定義,官方解釋為:

A Zone is an execution context that persists across async tasks, and allows the creator of the zone to observe and control execution of the code within the zone.

簡單來說,一個 zone 可作為多個非同步任務執行的上下文,並能夠控制這些非同步任務。詳細可以檢視這篇文章。

我們迴歸 Angular 框架,Angular 團隊通過對 zone.js 封裝,實現了 髒檢查Change Detection)機制。

當優化效能的時候,我們首先要考慮,哪些方面會影響程式的效能?總結主要有以下幾點:

  • 邏輯程式碼的複雜程度
    這主要是受js/ts運算效率低的影響,所以專案中儘量減少使用js/ts做複雜運算。即使有 牛xV8 引擎加持,還是要慎重,畢竟 javascript 是指令碼語言,執行效率與生俱來的低,哪怕你可以用它寫一個作業系統,哈哈哈!!

  • 減少Event Handler
    Event Handler的執行時間無疑對我們的效能有著重要的影響,比如 scrolbar和滑鼠移動事件,視窗resize事件,這些都是觸發頻率很高的事件,極其影響效能,可以對其做 節流防抖,以提升效能。

  • DOM 樹複雜
    如果你的專案是一個大型專案,且 DOM元素錯綜複雜,那麼你應該考慮如何拆解並將其元件化。

  • Angular DOM檢視更新
    大家都知道, Angular的牛逼之處就是友好的雙向繫結機制,但是繫結的值何時發生變化?何時更新?這也是這篇文章要達到的目的。

接下來我們簡單介紹一下 髒檢測(Change Detection)

,簡稱 CDAngular 預設是髒檢查方法是從根元件開始,遍歷所有的子元件進行髒檢查。我們看一個 檢測前檢測時 的模型。

髒檢測之前

圖中為元件樹

變化檢測時

那麼何時觸發髒檢測?主要有以下幾個方面:

  • ajax請求
  • timeout 延遲事件
  • 滑鼠事件

觸發髒檢測的目的就是 檢測檢視(DOM) 有沒有發生變化,方法就是比較 雙向繫結中 viewmodel 是否一致。

可能看到這的童鞋還是有一些暈叉叉,接下來我們通過一個例子來分析。

Demo

  1. 建立一個新的 Angular 專案:
ng new testZone
  1. 建立一個子元件 view:
ng g c view

view.component.html檔案

<p>
  {{user.getName()}}
</p>

<p>
  {{user.getAge()}}
</p>

view.component.ts

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

@Component({
  selector: 'app-view',
  templateUrl: './view.component.html',
  styleUrls: ['./view.component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ViewComponent implements DoCheck {

  @Input() user: any;

  index = 0;
  constructor() { }

  ngDoCheck() {
    this.index++;
    console.log('view被執行', this.index);
  }
}
  1. 修改app.component.ts元件
import { Component, DoCheck } from '@angular/core';

class User {
  _age = 25;
  _name = 'vincent';

  getAge() {
    console.log('執行獲取age');
    return this._age;
  }

  getName() {
    console.log('執行獲取name');
    return this._name;
  }
}


@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements DoCheck {

  index = 0;

  user = new User;
  change() {
  }

  ngDoCheck() {
    this.index++;
    console.log('app被執行', this.index);
  }
}

app.component.html

<!--The content below is only a placeholder and can be replaced.-->
<app-view [user]="user"></app-view>

<button (click)="change()">Click </button>
  1. 執行
npm start

訪問 http://localhost:4200/

chrome 控制檯:

圖中子元件 view模板內的user.getName()user.getAge()分別被執行四次,這裡要介紹一下,由於我們是開發環境,所以user.getName()user.getAge()會被執行兩次,當初始化的時候,觸發 Change Detection髒檢測,(Angular 內部會呼叫 detectChanges 這個方法),開發模式下,Angular 還會執行 checkNoChanges 這個方法去檢驗之前執行的Change Detection是否導致了別的改動。

但是為何 ngDoCheck 鉤子也被執行了兩次?

這是因為在 元件初始化的時候,ngDoCheck被呼叫了一次,
又因為執行了一次 髒檢測 又呼叫了一次 ngDoCheck

對比編譯後的結果:

編譯後還是執行了兩次,在這方面,我認為 Angular 這種檢測機制實在是很影響效能,有點雞肋了。。。。更雞肋的往下看。

此時我們 點選 按鈕,再檢視一下chrome控制檯:

我們可以發現,這個按鈕並沒有任何實際作用,但是 Angular 還是將getName()與getAge()又執行了一遍,設想一下,如果你有很多元件,點選按鈕後,檢測了所有元件,那麼這個效率?你認為會很高嗎?

優化檢測

類似的效率問題,主要有三種優化方法,逐一介紹。

使用ChangeDetectionStrategy.OnPush

view.component.ts檔案增加ChangeDetectionStrategy.OnPush,如下:

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

@Component({
  selector: 'app-view',
  templateUrl: './view.component.html',
  styleUrls: ['./view.component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ViewComponent implements OnInit, DoCheck {

  @Input() user: any;

  index = 0;
  constructor() { }

  ngOnInit() {
  }

  ngDoCheck() {
    this.index++;
    console.log('view被執行', this.index);
  }
}

重新執行結果如圖:

雖然 ngDoCheck 鉤子被執行兩次,但是getName()與getAge()函式只執行了一次, Nice!!!,鉤子函式在生產環境中要避免使用,即使使用也要儘量簡單。

此時再點選 按鈕,結果如圖:

只是 ngDoCheck 鉤子被執行了,這樣就起到了優化的作用。

那麼我們介紹一下ChangeDetectionStrategy.OnPushAngular 提供了相關機制可以讓我們去“告訴”它應當在什麼時候去執行 Change Detection。我們可以對應用中的每一個component設定Change Detection的策略。每個元件預設情況下的設定是ChangeDetectionStrategy.Default。意思就是任何事件都會導致元件被重新檢測。

ChangeDetectionStrategy還有一種策略叫做OnPush,如果當前Component設定成了OnPush,那麼當由當前Component之外的事件觸發的Change Detection在準備檢查當前Component之前,會先去檢查該ComponentInput,如果發現Input沒有變化,Change Detection會跳過這個Component和其Child Component

預警--------------------------!!!!!!!

在使用ChangeDetectionStrategy.OnPush時需要注意,只有兩種情況下Change Detection會在該Component內部執行檢測:

  • Input發生變化
  • 由Component內部的事件引起的Change Detection

所以在開發的時候,要清楚元件發生變化的情況。

----------------------------------------

我們還是通過一個例子來感受一下:

  1. 新建立一個view.service.ts服務。

    ng g s view/view
    

    內容為:

    import { Injectable } from '@angular/core';
    import { Subject } from 'rxjs';
    @Injectable({
      providedIn: 'root'
    })
    export class ViewService {
    
      subject = new Subject<number>();
      constructor() { }
    
      send(count: number) {
        this.subject.next(count);
      }
    }
    
  2. 修改app.component.tsapp.component.html

    import { Component, DoCheck } from '@angular/core';
    import { ViewService } from './view/view.service';
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent implements DoCheck {
      index = 0;
      constructor(private viewService: ViewService) {
      }
      change() {
        this.viewService.send(100);
      }
    
      ngDoCheck() {
        this.index++;
        console.log('app被執行', this.index);
      }
    }
    
    <app-view></app-view>
    <button (click)="change()">Click </button>
    
  3. 修改view.component.tsview.component.html

    import { Component, OnInit, DoCheck, ChangeDetectionStrategy } from '@angular/core';
    import { ViewService } from './view.service';
    
    @Component({
      selector: 'app-view',
      templateUrl: './view.component.html',
      styleUrls: ['./view.component.css'],
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class ViewComponent implements OnInit, DoCheck {
    
      index = 0;
      count = 0;
      constructor(private viewService: ViewService) {
        this.viewService.subject.asObservable().subscribe(
          (count) => {
            this.count = count;
          }
        );
      }
    
      ngOnInit() {
      }
    
      ngDoCheck() {
        this.index++;
        console.log('view被執行', this.index);
      }
    }
    
    <p>
      {{count}}
    </p>
    

上一個例子我們是通過@Input來觸發檢測,現在我們在使用了ChangeDetectionStrategy.OnPush後,通過rxjs訂閱的方式來改變顯示,執行如下:

chrome控制檯結果:

此時我們點選 按鈕,並沒有改變count的數值,不是100而是0,這時為什麼?

還記得上面的預警吧,只有在下面兩種情況下才會檢測:

  • Input發生變化
  • 由Component內部的事件引起的Change Detection

所以現在該怎麼解決?

幸運的是,Angular提供了ChangeDetectorRef這個類。我們可以將其注入需要呼叫的component,自己控制Change Detection的發生。

修改view.component.ts檔案如下:

import { Component, OnInit, DoCheck, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { ViewService } from './view.service';

@Component({
  selector: 'app-view',
  templateUrl: './view.component.html',
  styleUrls: ['./view.component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ViewComponent implements OnInit, DoCheck {

  index = 0;
  count = 0;
  constructor(private viewService: ViewService,
              private cdRef: ChangeDetectorRef) {
    this.viewService.subject.asObservable().subscribe(
      (count) => {
        this.count = count;
        this.cdRef.detectChanges();
      }
    );
  }

  ngOnInit() {
  }

  ngDoCheck() {
    this.index++;
    console.log('view被執行', this.index);
  }
}

重新執行後,點選 按鈕發生了變化。

除了detectChanges之外,ChangeDetectorRef中的markForCheck也可以解決我們的問題。不過和detectChanges不同的是,markForCheck會對在整個應用範圍內都進行Change Detection。具體使用哪一個,取決於我們的實際需求了。

運用Pipe

PipeAngular提供的又一個非常有用的功能,我們可以在模板中通過Pipe對資料進行轉換。

首先要說明的是Angular有兩種Pipe

  • Pure Pipe:如果傳入Pipe的引數沒有變,會直接返回之前一次的結果
  • InPure Pipe:每一次Change Detection都會重新執行Pipe內部的邏輯並返回結果

所以我們可以不使用ChangeDetectionStrategy.OnPush,只要把例子中 user.getName()user.getAge()兩個方法放到 Pipe 中即可,個人認為不是很好用。。

在ngFor迴圈中使用trackBy

這個優化本文並沒有用到,相信使用過Angular的你一定需要知道。

Angular在更新DOM的時候會刪除所有和ngFor中資料相關的DOM,然後再去根據新資料去重新挨個建立DOM。在列表很大時,這無疑是一個非常昂貴又耗時的操作。我們想要的行為,應當保留新老陣列中都存在的資料,對於其他資料進行刪除或新增。

Angular提供了trackBy方法去幫我們實現這樣的效果。trackBy方法的第一個引數是當前元素在陣列中的index,第二個是該元素本身。方法的返回值是當前資料的唯一標識。

例如有一個books陣列,陣列內是book物件,裡面有唯一的name屬性,唯一就是每個bookname是不一樣的,這樣我們就可以做優化。

  <ul>
      <li *ngFor="let book of books; trackBy: trackByFn">
        {{ book.name }}
      </li>
</ul>

...

  trackByFn(index, book) {
    return book.name;
  }

這樣在books陣列內某個book發生變化時,Angular會檢測它的name屬性是否發生變化,沒有發生變化保留DOM。被修改的namebook才會刪除修改DOM

-------------------------------

END