NgForOf 指令作用
该指令用于基于可迭代对象中的每一项创建相应的模板。每个实例化模板的上下文对象继承于外部的上下文对象,其值与可迭代对象对应项的值相关联。
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
提供了几个导出值,可以将其替换为局部变量:
$implicit: T - 表示
ngForOf
绑定的可迭代对象中的每一个独立项。ngForOf: NgIterable<T> - 表示迭代表达式的值。
index: number - 表示当前项的索引值。
first: boolean - 若当前项是可迭代对象的第一项,则返回 true。
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
选项需绑定到一个包含 index
和 item
两个参数的函数对象。若设定了 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>原文链接:https://www.f2er.com/angularjs/147219.html