MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Angular内置指令ngFor的使用技巧

2021-04-261.2k 阅读

ngFor 基础用法

在 Angular 中,ngFor 是一个非常重要的内置指令,用于在模板中基于一个数组或可迭代对象进行重复渲染。其基本语法如下:

<ul>
  <li *ngFor="let item of items">{{ item }}</li>
</ul>

在上述代码中,items 是组件类中定义的一个数组。let item 声明了一个局部变量 item,在每次迭代时,item 会被赋值为 items 数组中的当前元素。

在组件类中,items 数组可能像这样定义:

import { Component } from '@angular/core';

@Component({
  selector: 'app-example',
  templateUrl: './example.component.html',
  styleUrls: ['./example.component.css']
})
export class ExampleComponent {
  items = ['apple', 'banana', 'cherry'];
}

这将在页面上渲染出一个无序列表,列表项分别为 applebananacherry

ngFor 与索引

有时候,我们不仅需要访问数组中的元素,还需要知道元素的索引。ngFor 提供了获取索引的方式,语法如下:

<ul>
  <li *ngFor="let item of items; let i = index">{{ i + 1 }}. {{ item }}</li>
</ul>

这里,let i = index 声明了一个局部变量 i,它将在每次迭代中被赋值为当前元素的索引。索引从 0 开始,所以在显示时我们加了 1 以获得从 1 开始的编号。

遍历对象

ngFor 不仅可以遍历数组,还可以遍历对象。不过需要注意的是,遍历对象时,我们通常会得到对象的键值对。

<div *ngFor="let [key, value] of myObject | keyvalue">
  {{ key }}: {{ value }}
</div>

在组件类中:

import { Component } from '@angular/core';

@Component({
  selector: 'app-object-iteration',
  templateUrl: './object-iteration.component.html',
  styleUrls: ['./object-iteration.component.css']
})
export class ObjectIterationComponent {
  myObject = {
    name: 'John',
    age: 30,
    city: 'New York'
  };
}

这里使用了 keyvalue 管道,它将对象转换为一个包含 { key, value } 格式的数组,以便 ngFor 进行遍历。

ngFor 中的 TrackBy 函数

当使用 ngFor 渲染大量数据时,性能是一个重要问题。如果数组中的元素频繁变化,Angular 默认需要重新渲染整个列表。为了提高性能,可以使用 trackBy 函数。

trackBy 函数接受两个参数:索引 index 和当前元素 item。它应该返回一个唯一标识当前元素的值。

<ul>
  <li *ngFor="let item of items; trackBy: trackByFn">{{ item }}</li>
</ul>

在组件类中:

import { Component } from '@angular/core';

@Component({
  selector: 'app-trackby',
  templateUrl: './trackby.component.html',
  styleUrls: ['./trackby.component.css']
})
export class TrackbyComponent {
  items = [
    { id: 1, name: 'apple' },
    { id: 2, name: 'banana' },
    { id: 3, name: 'cherry' }
  ];

  trackByFn(index: number, item: any): number {
    return item.id;
  }
}

这样,当数组中的元素发生变化时,Angular 可以通过 trackByFn 返回的唯一标识来高效地更新列表,而不是重新渲染整个列表。

ngFor 嵌套

在实际应用中,可能会遇到需要嵌套 ngFor 的情况。例如,有一个二维数组,我们需要渲染一个表格。

<table>
  <tr *ngFor="let row of matrix">
    <td *ngFor="let cell of row">{{ cell }}</td>
  </tr>
</table>

在组件类中:

import { Component } from '@angular/core';

@Component({
  selector: 'app-nested-ngfor',
  templateUrl: './nested-ngfor.component.html',
  styleUrls: ['./nested-ngfor.component.css']
})
export class NestedNgforComponent {
  matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
  ];
}

这将渲染出一个简单的 3x3 表格。

ngFor 与 ngIf 的结合使用

在某些情况下,我们可能需要根据条件来决定是否渲染 ngFor 生成的元素。这时候可以将 ngForngIf 结合使用。

<ul>
  <li *ngFor="let item of items" *ngIf="item.visible">{{ item.name }}</li>
</ul>

在组件类中:

import { Component } from '@angular/core';

@Component({
  selector: 'app-ngfor-ngIf',
  templateUrl: './ngfor-ngIf.component.html',
  styleUrls: ['./ngfor-ngIf.component.css']
})
export class NgforNgIfComponent {
  items = [
    { name: 'apple', visible: true },
    { name: 'banana', visible: false },
    { name: 'cherry', visible: true }
  ];
}

这里,只有 visible 属性为 true 的元素才会被渲染。

避免在 ngFor 中使用函数调用

虽然在 ngFor 模板表达式中调用函数看起来很方便,但这可能会导致性能问题。因为每次变化检测时,函数都会被重新调用。

例如,避免这样的写法:

<ul>
  <li *ngFor="let item of items">{{ getFormattedValue(item) }}</li>
</ul>

在组件类中:

import { Component } from '@angular/core';

@Component({
  selector: 'app-avoid-func-call',
  templateUrl: './avoid-func-call.component.html',
  styleUrls: ['./avoid-func-call.component.css']
})
export class AvoidFuncCallComponent {
  items = [1, 2, 3];

  getFormattedValue(value: number): string {
    return `Value: ${value}`;
  }
}

更好的做法是在组件类中预先计算好需要的值,然后在模板中直接使用。

import { Component } from '@angular/core';

@Component({
  selector: 'app-avoid-func-call',
  templateUrl: './avoid-func-call.component.html',
  styleUrls: ['./avoid-func-call.component.css']
})
export class AvoidFuncCallComponent {
  items = [1, 2, 3];
  formattedValues: string[] = [];

  constructor() {
    this.items.forEach(item => {
      this.formattedValues.push(`Value: ${item}`);
    });
  }
}

模板中:

<ul>
  <li *ngFor="let value of formattedValues">{{ value }}</li>
</ul>

ngFor 与事件绑定

ngFor 生成的元素上可以绑定各种事件,例如点击事件。

<ul>
  <li *ngFor="let item of items" (click)="onItemClick(item)">{{ item }}</li>
</ul>

在组件类中:

import { Component } from '@angular/core';

@Component({
  selector: 'app-ngfor-event',
  templateUrl: './ngfor-event.component.html',
  styleUrls: ['./ngfor-event.component.css']
})
export class NgforEventComponent {
  items = ['apple', 'banana', 'cherry'];

  onItemClick(item: string) {
    console.log(`Clicked on ${item}`);
  }
}

这样,当点击列表项时,onItemClick 方法会被调用,并传入被点击的元素。

ngFor 中的样式绑定

可以根据 ngFor 中的元素状态或索引来绑定样式。

<ul>
  <li *ngFor="let item of items; let i = index" [ngClass]="{ 'active': i === 0 }">{{ item }}</li>
</ul>

在 CSS 文件中:

.active {
  color: red;
}

这里,第一个列表项会应用 active 类,从而显示为红色。

ngFor 与模板引用变量

模板引用变量在 ngFor 中也很有用。例如,我们可以获取 ngFor 生成的所有元素的引用。

<ul>
  <li *ngFor="let item of items; let i = index" #listItem>{{ item }}</li>
</ul>
<button (click)="logListItemCount()">Log item count</button>

在组件类中:

import { Component, ViewChildren } from '@angular/core';
import { QueryList } from '@angular/core';

@Component({
  selector: 'app-ngfor-template-ref',
  templateUrl: './ngfor-template-ref.component.html',
  styleUrls: ['./ngfor-template-ref.component.css']
})
export class NgforTemplateRefComponent {
  items = ['apple', 'banana', 'cherry'];

  @ViewChildren('listItem') listItems: QueryList<any>;

  logListItemCount() {
    console.log(`Number of list items: ${this.listItems.length}`);
  }
}

这里,@ViewChildren 装饰器用于获取所有带有 #listItem 模板引用变量的元素。

ngFor 与动态数据更新

ngFor 依赖的数组数据发生动态变化时,Angular 会自动检测并更新视图。例如,我们可以通过按钮点击来添加或删除数组元素。

<ul>
  <li *ngFor="let item of items">{{ item }}</li>
</ul>
<button (click)="addItem()">Add item</button>
<button (click)="removeItem()">Remove item</button>

在组件类中:

import { Component } from '@angular/core';

@Component({
  selector: 'app-dynamic-ngfor',
  templateUrl: './dynamic-ngfor.component.html',
  styleUrls: ['./dynamic-ngfor.component.css']
})
export class DynamicNgforComponent {
  items = ['apple', 'banana'];

  addItem() {
    this.items.push('cherry');
  }

  removeItem() {
    if (this.items.length > 0) {
      this.items.pop();
    }
  }
}

每次点击按钮,数组会相应地添加或删除元素,ngFor 会自动更新视图。

ngFor 中的错误处理

在使用 ngFor 时,可能会遇到一些错误情况,例如数组未定义。为了避免错误,可以在模板中进行安全检查。

<ul>
  <li *ngFor="let item of items || []">{{ item }}</li>
</ul>

这里使用了 || [],如果 items 未定义,会使用一个空数组,从而避免 ngFor 报错。

深入理解 ngFor 的变化检测机制

Angular 的变化检测机制在 ngFor 的运行中起着关键作用。当 ngFor 所依赖的数组发生变化时,Angular 会触发变化检测。默认情况下,Angular 使用的是默认的变化检测策略,即当组件树中的任何数据发生变化时,整个组件树都会进行变化检测。

对于 ngFor 来说,这意味着如果数组中的任何元素发生变化,或者数组本身的长度发生变化,ngFor 都会重新评估并更新视图。但是,这种全面的检查在处理大量数据时可能会导致性能问题。

为了优化性能,我们可以使用 OnPush 变化检测策略。当一个组件使用 OnPush 策略时,Angular 只会在以下情况下触发该组件的变化检测:

  1. 组件接收到新的输入属性(@Input())。
  2. 组件触发了一个事件(如点击事件)。
  3. Observable 发出新的值并且该 Observable 被绑定到组件的输入属性或在组件的 async 管道中使用。

例如,我们可以这样定义一个使用 OnPush 策略的组件:

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

@Component({
  selector: 'app-onpush-ngfor',
  templateUrl: './onpush-ngfor.component.html',
  styleUrls: ['./onpush-ngfor.component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPushNgforComponent {
  items = ['apple', 'banana', 'cherry'];
}

在这个组件中,如果 items 数组的内容发生变化,但数组的引用没有改变(例如通过直接修改数组元素而不是重新赋值整个数组),默认情况下,变化检测不会触发,视图也不会更新。

为了在 OnPush 策略下让 ngFor 正确响应数组变化,我们需要确保数组的引用发生变化。一种常见的做法是使用不可变数据模式。例如,使用 mapfilterconcat 等数组方法来创建一个新的数组。

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

@Component({
  selector: 'app-onpush-ngfor',
  templateUrl: './onpush-ngfor.component.html',
  styleUrls: ['./onpush-ngfor.component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPushNgforComponent {
  items = ['apple', 'banana', 'cherry'];

  updateItems() {
    // 使用 map 方法创建一个新的数组
    this.items = this.items.map(item => item + ' updated');
  }
}

这样,当调用 updateItems 方法时,items 数组的引用发生了变化,OnPush 变化检测策略会检测到这个变化,从而更新 ngFor 渲染的视图。

ngFor 在响应式设计中的应用

在响应式设计中,ngFor 可以根据不同的屏幕尺寸动态地渲染不同数量或样式的元素。我们可以结合 Angular 的响应式布局模块和 ngFor 来实现这一点。

首先,我们需要安装并导入 @angular/flex - layout 库,它提供了响应式布局的功能。

npm install @angular/flex - layout

然后在模块中导入:

import { FlexLayoutModule } from '@angular/flex - layout';

@NgModule({
  imports: [
    FlexLayoutModule
  ],
  declarations: [AppComponent],
  bootstrap: [AppComponent]
})
export class AppModule {}

在模板中,我们可以根据屏幕尺寸来决定 ngFor 渲染的元素数量。

<div fxLayout="row" fxLayout.xs="column" fxLayoutAlign="center center">
  <div *ngFor="let item of items.slice(0, getLimit())" fxFlex="33%" fxFlex.xs="100%">
    {{ item }}
  </div>
</div>

在组件类中:

import { Component } from '@angular/core';
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
import { Observable } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators';

@Component({
  selector: 'app-responsive-ngfor',
  templateUrl: './responsive-ngfor.component.html',
  styleUrls: ['./responsive-ngfor.component.css']
})
export class ResponsiveNgforComponent {
  items = ['item1', 'item2', 'item3', 'item4', 'item5', 'item6'];
  isHandset$: Observable<boolean> = this.breakpointObserver.observe(Breakpoints.Handset)
   .pipe(
      map(result => result.matches),
      shareReplay()
    );

  getLimit(): number {
    let limit = 3;
    this.isHandset$.subscribe(isHandset => {
      if (isHandset) {
        limit = 1;
      }
    });
    return limit;
  }
}

这里,通过 BreakpointObserver 来检测屏幕是否为手机尺寸。如果是手机尺寸,ngFor 只渲染一个元素,否则渲染三个元素。同时,通过 fxLayoutfxFlex 指令来实现响应式的布局。

ngFor 与动画结合

Angular 的动画模块可以与 ngFor 结合,为列表项添加动画效果。例如,我们可以为列表项的添加和移除添加动画。

首先,在模块中导入动画模块:

import { BrowserAnimationsModule } from '@angular/platform - browser/animations';

@NgModule({
  imports: [
    BrowserAnimationsModule
  ],
  declarations: [AppComponent],
  bootstrap: [AppComponent]
})
export class AppModule {}

在组件类中定义动画:

import { Component, trigger, state, style, animate, transition } from '@angular/animations';

@Component({
  selector: 'app-ngfor - animations',
  templateUrl: './ngfor - animations.component.html',
  styleUrls: ['./ngfor - animations.component.css'],
  animations: [
    trigger('itemAnim', [
      state('void', style({ opacity: 0 })),
      transition(':enter', [
        style({ opacity: 0 }),
        animate(300, style({ opacity: 1 }))
      ]),
      transition(':leave', [
        animate(300, style({ opacity: 0 }))
      ])
    ])
  ]
})
export class NgforAnimationsComponent {
  items = ['apple', 'banana', 'cherry'];

  addItem() {
    this.items.push('new item');
  }

  removeItem() {
    if (this.items.length > 0) {
      this.items.pop();
    }
  }
}

在模板中应用动画:

<ul>
  <li *ngFor="let item of items" [@itemAnim]>{{ item }}</li>
</ul>
<button (click)="addItem()">Add item</button>
<button (click)="removeItem()">Remove item</button>

这里,@itemAnim 是我们定义的动画触发器。当列表项添加时,会有一个淡入的动画效果;当列表项移除时,会有一个淡出的动画效果。

ngFor 在表单中的应用

在表单中,ngFor 可以用于动态生成表单控件。例如,我们要创建一个包含多个输入框的表单,每个输入框对应一个数组中的元素。

首先,在组件类中导入表单相关的模块并定义表单:

import { Component } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';

@Component({
  selector: 'app-ngfor - in - form',
  templateUrl: './ngfor - in - form.component.html',
  styleUrls: ['./ngfor - in - form.component.css']
})
export class NgforInFormComponent {
  myForm: FormGroup;
  items = ['field1', 'field2', 'field3'];

  constructor() {
    this.myForm = new FormGroup({});
    this.items.forEach((item, index) => {
      this.myForm.addControl(`control${index}`, new FormControl('', Validators.required));
    });
  }
}

在模板中使用 ngFor 生成表单控件:

<form [formGroup]="myForm">
  <div *ngFor="let item of items; let i = index">
    <label [for]="'control' + i">{{ item }}</label>
    <input type="text" [id]="'control' + i" [formControlName]="'control' + i">
  </div>
  <button type="submit" [disabled]="!myForm.valid">Submit</button>
</form>

这里,通过 ngFor 为每个 items 中的元素生成一个输入框,并使用 FormControlName 指令将输入框与表单控件绑定。同时,表单会根据输入内容的有效性来控制提交按钮的状态。

ngFor 与服务端数据交互

在实际应用中,ngFor 通常会与服务端数据交互结合使用。我们可以通过 Angular 的 HttpClient 从服务端获取数据,然后使用 ngFor 进行渲染。

首先,创建一个服务来获取数据:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class DataService {
  constructor(private http: HttpClient) {}

  getData(): Observable<any[]> {
    return this.http.get<any[]>('api/data');
  }
}

在组件类中注入服务并获取数据:

import { Component } from '@angular/core';
import { DataService } from './data.service';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-ngfor - server - data',
  templateUrl: './ngfor - server - data.component.html',
  styleUrls: ['./ngfor - server - data.component.css']
})
export class NgforServerDataComponent {
  items$: Observable<any[]>;

  constructor(private dataService: DataService) {
    this.items$ = this.dataService.getData();
  }
}

在模板中使用 async 管道结合 ngFor 来渲染数据:

<ul>
  <li *ngFor="let item of items$ | async">{{ item.name }}</li>
</ul>

这里,async 管道会订阅 Observable,并在数据可用时将其传递给 ngFor 进行渲染。同时,当组件销毁时,async 管道会自动取消订阅,避免内存泄漏。

ngFor 与 SEO

在进行前端开发时,搜索引擎优化(SEO)也是一个重要的考虑因素。虽然 ngFor 本身不会直接影响 SEO,但我们可以通过一些方法来确保使用 ngFor 渲染的内容对搜索引擎友好。

  1. 正确使用语义化标签:在使用 ngFor 渲染列表时,尽量使用语义化标签,如 <ul><ol> 等。搜索引擎可以更好地理解这些标签的含义,从而更准确地对页面内容进行索引。
<ul>
  <li *ngFor="let item of items">{{ item }}</li>
</ul>
  1. 提供有意义的内容:确保 ngFor 渲染的内容本身是有意义的,并且包含相关的关键词。例如,如果是一个产品列表,每个列表项应包含产品名称、描述等重要信息。
<ul>
  <li *ngFor="let product of products">
    <h3>{{ product.name }}</h3>
    <p>{{ product.description }}</p>
  </li>
</ul>
  1. 处理动态内容:搜索引擎爬虫通常不会执行 JavaScript,所以对于通过 ngFor 动态加载的内容,可能无法正确抓取。一种解决方法是使用服务器端渲染(SSR),在服务器端生成完整的 HTML 页面,这样搜索引擎可以直接获取到渲染后的内容。

ngFor 性能优化的更多技巧

  1. 减少 DOM 操作ngFor 会频繁地操作 DOM,尤其是在数据变化时。尽量减少不必要的数据变化,例如通过防抖或节流技术来处理用户输入,避免频繁触发 ngFor 的变化检测。
  2. 虚拟滚动:对于大数据集,使用虚拟滚动库(如 @angular/cdk/scrolling 中的 VirtualScroll)可以显著提高性能。虚拟滚动只渲染当前可见区域的列表项,而不是全部渲染,大大减少了 DOM 元素的数量。
  3. 优化数据结构:确保 ngFor 所依赖的数组数据结构是最优的。例如,如果需要频繁查找或删除元素,可以考虑使用更适合的数组方法或数据结构,如 MapSet 结合数组来提高操作效率。

ngFor 与单元测试

在进行 Angular 应用的单元测试时,涉及 ngFor 的组件也需要进行正确的测试。我们可以使用 Angular 的测试工具,如 TestBedComponentFixture 来测试 ngFor 的功能。

假设我们有一个简单的组件使用 ngFor 渲染列表:

import { Component } from '@angular/core';

@Component({
  selector: 'app - ngfor - test',
  templateUrl: './ngfor - test.component.html',
  styleUrls: ['./ngfor - test.component.css']
})
export class NgforTestComponent {
  items = ['apple', 'banana', 'cherry'];
}

模板:

<ul>
  <li *ngFor="let item of items">{{ item }}</li>
</ul>

单元测试代码如下:

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NgforTestComponent } from './ngfor - test.component';

describe('NgforTestComponent', () => {
  let component: NgforTestComponent;
  let fixture: ComponentFixture<NgforTestComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [NgforTestComponent]
    })
   .compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(NgforTestComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should render list items', () => {
    const compiled = fixture.nativeElement as HTMLElement;
    const listItems = compiled.querySelectorAll('li');
    expect(listItems.length).toBe(component.items.length);
    component.items.forEach((item, index) => {
      expect(listItems[index].textContent).toContain(item);
    });
  });
});

在这个测试中,我们首先使用 TestBed 配置并编译组件。然后,通过 ComponentFixture 创建组件实例并触发变化检测。最后,我们检查渲染后的列表项数量和内容是否与组件中的数据一致。

通过以上对 ngFor 各种使用技巧和深入理解的介绍,开发者可以在 Angular 项目中更高效、灵活地使用 ngFor,提升应用的性能和用户体验。无论是简单的列表渲染,还是复杂的动态数据处理、与其他功能模块的结合,ngFor 都有着广泛的应用场景,熟练掌握这些技巧将有助于打造优秀的前端应用。