• AngularX 指令(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 选项需绑定到一个包含 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>
    世事如棋,乾坤莫测,笑尽英雄啊!
  • 相关阅读:
    如何快速查看archlinux pacman 软件记录?
    如何快速判断检查主机ip端口是否存活?
    如何在不清楚有哪些tag的情况下拉取docker镜像?
    如何使用docker版Openresty测试框架Test::Nginx进行测试?
    .net mvc4 使用 System.Web.Optimization 对javascript和style的引入、代码合并和压缩的优化(ScriptBundle,StyleBundle,Bund
    zen coding de 一些快捷功能
    C# Xpath
    一个SQL SERVER查询分析器非常好用的工具
    探索MSSQL执行计划
    博客园武林人士
  • 原文地址:https://www.cnblogs.com/bsjl/p/7741863.html
Copyright © 2020-2023  润新知