Angular 4.x NgForOf

NgForOf 指令做用

該指令用於基於可迭代對象中的每一項建立相應的模板。每一個實例化模板的上下文對象繼承於外部的上下文對象,其值與可迭代對象對應項的值相關聯。html

NgForOf 指令語法

* 語法糖

<li *ngFor="let item of items; index as i; trackBy: trackByFn">...</li>

template語法

<li template="ngFor let item of items; index as i; trackBy: trackByFn">...</li>

<ng-template> 元素

<ng-template ngFor let-item [ngForOf]="items" let-i="index" [ngForTrackBy]="trackByFn">
  <li>...</li>
</ng-template>
<!--等價於-->
<ng-template ngFor let-item="$implicit" [ngForOf]="items" let-i="index" 
   [ngForTrackBy]="trackByFn">
  <li>...</li>
</ng-template>

NgForOf 使用示例

@Component({
  selector: 'exe-app',
  template: `
    <ul>
      <li *ngFor="let item of items; let i = index">
        {{i}}. {{item}}
      </li>
    </ul>
  `
})
export class AppComponent {
  items = ['First', 'Second', 'Third'];
}

基礎知識

NgForOfContext

NgForOfContext 實例用於表示 NgForOf 上下文。算法

// packages/common/src/directives/ng_for_of.ts
export class NgForOfContext<T> {
  constructor(
      public $implicit: T, 
      public ngForOf: NgIterable<T>, 
      public index: number,
      public count: number) {}

  get first(): boolean { return this.index === 0; }

  get last(): boolean { return this.index === this.count - 1; }

  get even(): boolean { return this.index % 2 === 0; }

  get odd(): boolean { return !this.even; }
}

// 定義可迭代的類型
export type NgIterable<T> = Array<T>| Iterable<T>;

Local Variables

NgForOf 提供了幾個導出值,能夠將其替換爲局部變量:typescript

  • $implicit: T - 表示 ngForOf 綁定的可迭代對象中的每個獨立項。api

  • ngForOf: NgIterable<T> - 表示迭表明達式的值。數組

  • index: number - 表示當前項的索引值。app

  • first: boolean - 若當前項是可迭代對象的第一項,則返回 true。ide

  • last: boolean - 若當前項是可迭代對象的最後一項,則返回 true。函數

  • even: boolean - 若當前項的索引值是偶數,則返回 true。源碼分析

  • odd: boolean - 若當前項的索引值是奇數,則返回 true。性能

Change Propagation

當可迭代對象的值改變時,NgForOf 對 DOM 會進行相應的更改:

  • 當新增某一項,對應的模板實例將會被添加到 DOM

  • 當移除某一項,對應的模板實例將會從 DOM 中移除

  • 當對可迭代對象每一項進行從新排序,它們各自的模板將在 DOM 中從新排序

  • 不然,頁面中的 DOM 元素將保持不變。

Angular 使用對象標識來跟蹤可迭代對象中,每一項的插入和刪除,並在 DOM 中作出相應的變化。但使用對象標識有一個問題,假設咱們經過服務端獲取可迭代對象,當從新調用服務端接口獲取新數據時,儘管服務端返回的數據沒有變化,但它將產生一個新的對象。此時,Angular 將徹底銷燬可迭代對象相關的 DOM 元素,而後從新建立對應的 DOM 元素。這是一個很昂貴 (影響性能) 的操做,若是可能的話應該儘可能避免。

所以,Angular 提供了 trackBy 選項,讓咱們可以自定義跟蹤算法。 trackBy 選項需綁定到一個包含 indexitem 兩個參數的函數對象。若設定了 trackBy 選項,Angular 將基於函數的返回值來跟蹤變化。

IterableDiffers

用於跟蹤可迭代對象變化差別。

TrackByFunction

用於定義 trackBy 綁定函數的類型:

// packages/core/src/change_detection/differs/iterable_differs.ts
export interface TrackByFunction<T> { (index: number, item: T): any; }

SimpleChanges

用於表示變化對象,對象的 keys 是變化的屬性名,而對應的屬性值是 SimpleChange 對象。

// packages/core/src/metadata/lifecycle_hooks.ts
export interface SimpleChanges { [propName: string]: SimpleChange; }

SimpleChange

用於表示從舊值到新值的基本變化。

// packages/core/src/change_detection/change_detection_util.ts
export class SimpleChange {
  constructor(
     public previousValue: any, 
     public currentValue: any, 
     public firstChange: boolean) {}

  // 驗證是不是首次變化
  isFirstChange(): boolean { return this.firstChange; }
}

NgForOf 源碼分析

NgForOf 指令定義

@Directive({
  selector: '[ngFor][ngForOf]'
})

NgForOf 類私有屬性及構造函數

// packages/common/src/directives/ng_for_of.ts
export class NgForOf<T> implements DoCheck, OnChanges {
   private _differ: IterableDiffer<T>|null = null;
   private _trackByFn: TrackByFunction<T>;
   
   constructor(
      private _viewContainer: ViewContainerRef,
      private _template: TemplateRef<NgForOfContext<T>>,
      private _differs: IterableDiffers) {}
}

NgForOf 類輸入屬性

// <ng-template ngFor let-item [ngForOf]="items" let-i="index" [ngForTrackBy]="trackByFn">
@Input() ngForOf: NgIterable<T>; // 表示ngForOf屬性,綁定的可迭代對象
@Input()
set ngForTrackBy(fn: TrackByFunction<T>) {
    // 在開發模式下,若ngForTrackBy屬性綁定的對象不是函數類型,則提示用戶。
    if (isDevMode() && fn != null && typeof fn !== 'function') {
      // TODO(vicb): use a log service once there is a public one available
      if (<any>console && <any>console.warn) {
        console.warn(
            `trackBy must be a function, but received ${JSON.stringify(fn)}. ` +
            `See https://angular.io/docs/ts/latest/api/common/index/NgFor-
             directive.html#!#change-propagation for more information.`);
      }
    }
    this._trackByFn = fn;
}

@Input()
set ngForTemplate(value: TemplateRef<NgForOfContext<T>>) { // 表示ngFor對應的模板對象
    if (value) {
      this._template = value;
    }
}

NgForOf 指令生命週期

export class NgForOf<T> implements DoCheck, OnChanges {
  // 當輸入屬性發生變化,獲取ngForOf綁定的可迭代對象的當前值,若_differ對象未建立,則基於ngForTrackBy
  // 函數建立IterableDiffer對象。
  ngOnChanges(changes: SimpleChanges): void {
    if ('ngForOf' in changes) {
      // React on ngForOf changes only once all inputs have been initialized
      const value = changes['ngForOf'].currentValue;
      if (!this._differ && value) {
        try {
          this._differ = this._differs.find(value).create(this.ngForTrackBy);
        } catch (e) {
          throw new Error(
              `Cannot find a differ supporting object '${value}' of type 
              '${getTypeNameForDebugging(value)}'. NgFor only supports binding to 
                Iterables such as Arrays.`);
        }
      }
    }
  }
  
  // 調用IterableDiffer對象的diff()方法,計算可迭代對象變化的差別值,若發生變化則響應對應的變化。
  ngDoCheck(): void {
    if (this._differ) {
      const changes = this._differ.diff(this.ngForOf);
      if (changes) this._applyChanges(changes);
    }
  }
}

經過源碼咱們發如今 ngDoCheck() 方法中,會調用 IterableDiffer 對象的 diff() 方法計算變化差別。該方法返回 IterableChanges 對象。如今咱們來分析一下 IterableChanges 對象。

IterableChanges

IterableChanges 對象用於表示從上次調用 diff() 方法後,可迭代對象發生的變化。IterableChanges 接口定義以下:

// packages/core/src/change_detection/differs/iterable_differs.ts
export interface IterableChanges<V> {
  /** 迭代全部變化的項,IterableChangeRecord將包含每一項的變化信息 */
  forEachItem(fn: (record: IterableChangeRecord<V>) => void): void;
  
  /** 對原始的可迭代對象應用執行對應的操做,從而產生新的可迭代對象 */
  forEachOperation(fn: (record: IterableChangeRecord<V>, 
    previousIndex: number, currentIndex: number) => void): void;
  
  /** 迭代原始Iterable的順序的變化,顯示原始項目移動的位置。*/
  forEachPreviousItem(fn: (record: IterableChangeRecord<V>) => void): void;

  /** 迭代全部新增的項 */
  forEachAddedItem(fn: (record: IterableChangeRecord<V>) => void): void;

  /** 迭代已移動的項 */
  forEachMovedItem(fn: (record: IterableChangeRecord<V>) => void): void;

  /** 迭代已移除的項 */
  forEachRemovedItem(fn: (record: IterableChangeRecord<V>) => void): void;

  /** 迭代全部基於trackByFn函數標識的變化項 */
  forEachIdentityChange(fn: (record: IterableChangeRecord<V>) => void): void;
}

咱們注意到每一個迭代函數 fn 的輸入參數類型是 IterableChangeRecord 對象。IterableChangeRecord 接口定義以下:

// packages/core/src/change_detection/differs/iterable_differs.ts
export interface IterableChangeRecord<V> {
  /** Current index of the item in `Iterable` or null if removed. */
  readonly currentIndex: number|null;

  /** Previous index of the item in `Iterable` or null if added. */
  readonly previousIndex: number|null;

  /** The item. */
  readonly item: V;

  /** Track by identity as computed by the `trackByFn`. */
  readonly trackById: any;
}

分析完 diff() 方法返回IterableChanges 對象,接下來咱們來重點分析一下 _applyChanges 方法。

NgForOf 類私有方法

在介紹 NgForOf 類私有方法前,咱們須要先介紹一下 RecordViewTuple 類,該類用於記錄視圖的變化。 RecordViewTuple 類的定義以下:

class RecordViewTuple<T> {
  constructor(public record: any, 
  public view: EmbeddedViewRef<NgForOfContext<T>>) {}
}

介紹完 RecordViewTuple 類,咱們立刻來看一下 NgForOf 類中的私有方法:

private _applyChanges(changes: IterableChanges<T>) {
    const insertTuples: RecordViewTuple<T>[] = [];
    // 基於IterableChanges對象,執行視圖更新操做:如新增、刪除或移動操做。
    changes.forEachOperation(
        (item: IterableChangeRecord<any>, 
          adjustedPreviousIndex: number, 
          currentIndex: number) => {
          // 對於新增的項,previousIndex的值爲null
          if (item.previousIndex == null) {
          /**
           * export class NgForOfContext<T> {
           *  constructor(
             *    public $implicit: T, 
             *    public ngForOf: NgIterable<T>,
           *    public index: number,
           *    public count: number) {}
           * }
           */
            // 基於TemplateRef對象及NgForOfContext上下文建立內嵌視圖
            const view = this._viewContainer.createEmbeddedView(
                this._template, new NgForOfContext<T>(null !, this.ngForOf, -1, -1), 
                  currentIndex);
            const tuple = new RecordViewTuple<T>(item, view);
            insertTuples.push(tuple);
          } else if (currentIndex == null) { // 對於已移除的項,currentIndex的值爲null
            // 根據以前的索引值,在視圖容器中移除對應的視圖。
            this._viewContainer.remove(adjustedPreviousIndex);
          } else { 
            // 執行視圖的移動操做:先根據以前索引值獲取對應的視圖對象,而後將該視圖移動到currentIndex
            // 指定的位置上。同時建立一個新的RecordViewTuple對象,用於記錄該變化。
            const view = this._viewContainer.get(adjustedPreviousIndex) !;
            this._viewContainer.move(view, currentIndex);
            const tuple = new RecordViewTuple(item, 
              <EmbeddedViewRef<NgForOfContext<T>>>view);
            insertTuples.push(tuple);
          }
        });

    // 遍歷視圖變化記錄數組(記錄視圖新增與移動操做),更新每一項中EmbeddedViewRef對象,context屬性對應
    // 的上下文對象中$implicit屬性的值爲新的值。
    for (let i = 0; i < insertTuples.length; i++) {
      this._perViewChange(insertTuples[i].view, insertTuples[i].record);
    }

    // 遍歷視圖容器中的視圖,設置視圖上下文對象中的`index`和`count`的值。
    for (let i = 0, ilen = this._viewContainer.length; i < ilen; i++) {
      const viewRef = <EmbeddedViewRef<NgForOfContext<T>>>this._viewContainer.get(i);
      viewRef.context.index = i;
      viewRef.context.count = ilen;
    }

    // 迭代全部基於trackByFn函數標識的變化項,更新每一項中EmbeddedViewRef對象,context屬性對應
    // 的上下文對象中$implicit屬性的值爲新的值。
    changes.forEachIdentityChange((record: any) => {
      const viewRef =
         <EmbeddedViewRef<NgForOfContext<T>>>this._viewContainer.get(record.currentIndex);
      viewRef.context.$implicit = record.item;
    });
  }

private _perViewChange(
    view: EmbeddedViewRef<NgForOfContext<T>>, 
    record: IterableChangeRecord<any>) {
    view.context.$implicit = record.item;
}

NgForOf 指令的源碼已經分析完了,該指令的核心就是如何高效的跟蹤可迭代對象的變化,而後儘量複用已有的 DOM 元素,來提升應用的性能。後面若是有時間的話,會整理專門的文章來分析 IterableDiffer 對象 diff() 算法具體實現。

在調用 ViewContainerRef 對象的 createEmbeddedView() 方法建立視圖對象時,除了指定 TemplateRef 對象,咱們還能夠設置 TemplateRef 對象關聯的上下文對象及視圖的插入位置。其中上下文對象,用於做爲解析模板綁定表達式的上下文。最後咱們再來回顧一下如下語法,是否是感受清晰不少。

<ng-template ngFor let-item [ngForOf]="items" let-i="index" [ngForTrackBy]="trackByFn">
  <li>...</li>
</ng-template>
<!--等價於-->
<ng-template ngFor let-item="$implicit" [ngForOf]="items" let-i="index"
   [ngForTrackBy]="trackByFn">
  <li>...</li>
</ng-template>
相關文章
相關標籤/搜索