該指令用於基於可迭代對象中的每一項建立相應的模板。每一個實例化模板的上下文對象繼承於外部的上下文對象,其值與可迭代對象對應項的值相關聯。html
*
語法糖<li *ngFor="let item of items; index as i; trackBy: trackByFn">...</li>
<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>
@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 實例用於表示 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>;
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。性能
當可迭代對象的值改變時,NgForOf 對 DOM 會進行相應的更改:
當新增某一項,對應的模板實例將會被添加到 DOM
當移除某一項,對應的模板實例將會從 DOM 中移除
當對可迭代對象每一項進行從新排序,它們各自的模板將在 DOM 中從新排序
不然,頁面中的 DOM 元素將保持不變。
Angular 使用對象標識來跟蹤可迭代對象中,每一項的插入和刪除,並在 DOM 中作出相應的變化。但使用對象標識有一個問題,假設咱們經過服務端獲取可迭代對象,當從新調用服務端接口獲取新數據時,儘管服務端返回的數據沒有變化,但它將產生一個新的對象。此時,Angular 將徹底銷燬可迭代對象相關的 DOM 元素,而後從新建立對應的 DOM 元素。這是一個很昂貴 (影響性能) 的操做,若是可能的話應該儘可能避免。
所以,Angular 提供了 trackBy
選項,讓咱們可以自定義跟蹤算法。 trackBy
選項需綁定到一個包含 index
和 item
兩個參數的函數對象。若設定了 trackBy
選項,Angular 將基於函數的返回值來跟蹤變化。
用於跟蹤可迭代對象變化差別。
用於定義 trackBy 綁定函數的類型:
// packages/core/src/change_detection/differs/iterable_differs.ts export interface TrackByFunction<T> { (index: number, item: T): any; }
用於表示變化對象,對象的 keys
是變化的屬性名,而對應的屬性值是 SimpleChange
對象。
// packages/core/src/metadata/lifecycle_hooks.ts export interface SimpleChanges { [propName: string]: 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; } }
@Directive({ selector: '[ngFor][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) {} }
// <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; } }
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
對象用於表示從上次調用 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
類私有方法前,咱們須要先介紹一下 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>