上一章我们介绍了ngIf
,这是一个条件选择的指令。与此类似,Angular 还提供了用于循环的指令:ngFor
。ngFor
指令遍历一个数据集合,;例如数组、列表等,然后在 HTML 模板中为每一个数据项创建一个 HTML 元素。这个指令可以帮助我们以一种优雅的方式,构建一个列表或表格。本章,我们将详细介绍ngFor
指令。
ngFor
语法
ngFor
语法如下:
<html-element *ngFor="let item of items;”> <html-Template></html-Template> </html-element>
其中,<html-element>
作为ngFor
指令应用到的元素,它会在ngFor
的作用下,按照数据项进行循环。由于ngFor
是一个结构指令,因此也是以*
开头。ngFor
指令的内容let item of items
,items
是遍历的集合,item
是集合中的每一个元素。因此,ngFor
的含义类似于forEach
,即将items
中的每一个元素赋值给item
,也就是说,item
代表了当前的循环变量。从语法上说,item
是一个模板输入变量。items
通常是组件类的一个成员变量,或者你直接创建的集合,例如:
<html-element *ngFor="let item of [ 1, 2, 3, 4 ];”> <html-Template></html-Template> </html-element>
如果ngFor
内容只有这些,那么最后的分号就是可选的。不过我们很快就会看到,ngFor
指令还可以有更多内容。item
变量的作用域仅限于<html-element>
元素。你可以在<html-element>
元素中的任何位置只用这个变量,但不能在元素外部使用。
下面我们可以看一个ngFor
的简单的例子:
import { Component } from '@angular/core'; interface Movie { title: string; director: string; cast: string; releaseDate: string; } @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { title = 'Top 5 Movies'; movies: Movie[] = [ { title: 'Zootopia', director: 'Byron Howard, Rich Moore', cast: 'Idris Elba, Ginnifer Goodwin, Jason Bateman', releaseDate: 'March 4, 2016' }, { title: 'Batman v Superman: Dawn of Justice', director: 'Zack Snyder', cast: 'Ben Affleck, Henry Cavill, Amy Adams', releaseDate: 'March 25, 2016' }, { title: 'Captain American: Civil War', director: 'Anthony Russo, Joe Russo', cast: 'Scarlett Johansson, Elizabeth Olsen, Chris Evans', releaseDate: 'May 6, 2016' }, { title: 'X-Men: Apocalypse', director: 'Bryan Singer', cast: 'Jennifer Lawrence, Olivia Munn, Oscar Isaac', releaseDate: 'May 27, 2016' }, { title: 'Warcraft', director: 'Duncan Jones', cast: 'Travis Fimmel, Robert Kazinsky, Ben Foster', releaseDate: 'June 10, 2016' } ]; }
<h1> {{title}} </h1> <ul> <li *ngFor="let movie of movies"> {{ movie.title }} - {{movie.director}} </li> </ul>
首先,我们定义了一个interface Movie
,用于定义数据。TypeScript 的interface
与 Java 的类似,都是用于定义一种规范,不同之处在于,TypeScript 的interface
可以定义属性,用于规范数据的类型。在组件类中,我们利用前面定义的Movie
类型,以数组的形式给出了 2016 年的几部著名电影的信息。接下来的 HTML 代码中,我们使用ngFor
遍历这个数组,利用字符串绑定将数据输出到页面。
利用浏览器的审查元素功能,我们可以看到,Angular 是如何生成li
标签的:
下面我们看一个更复杂的例子,嵌套数组的遍历:
import { Component } from '@angular/core'; interface Employee { name: string; email: string; skills: { skill: string; exp: string; }[]; } @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { employees: Employee[] = [ { name: 'Rahul', email: 'rahul@gmail.com', skills: [{skill: 'Angular', exp: '2'}, {skill: 'Javascript', exp: '7'}, {skill: 'TypeScript', exp: '3'}] }, { name: 'Sachin', email: 'sachin@gmail.com', skills: [{skill: 'Angular', exp: '1'}, {skill: 'Android', exp: '3'}, {skill: 'React', exp: '2'}] }, { name: 'Laxmna', email: 'laxman@gmail.com', skills: [{skill: 'HTML', exp: '2'}, {skill: 'CSS', exp: '2'}, {skill: 'Javascript', exp: '1'}] } ]; }
<table> <thead> <tr> <th>Name</th> <th>Mail ID</th> <th>Skills</th> </tr> </thead> <tbody> <tr *ngFor="let employee of employees;"> <td>{{employee.name}}</td> <td>{{employee.email}}</td> <td> <table> <tbody> <tr *ngFor="let skill of employee.skills;"> <td>{{skill.skill}}</td> <td>{{skill.exp}}</td> </tr> </tbody> </table> </td> </tr> </tbody> </table>
与前面的例子类似,我们同样定义了interface Employee
接口,然后在 HTML 中,使用嵌套的<table>
标签进行渲染。这与前面的介绍并没有本质的区别。
局部变量
除了前面介绍的ngFor
的最基本语法,ngFor
还定义了很多局部变量,用于我们更方便的获知ngFor
的执行状态。我们可以在模板中使用这些局部变量。ngFor
定义的局部变量如下:
index: number
- 集合遍历的当前索引,从 0 开始count: number
- 集合元素总数first: boolean
- 是否是集合中第一个元素last: boolean
- 是否是集合中的最后一个元素even: boolean
- 是否是偶数索引位odd: boolean
- 是否是奇数索引位
之所以被称为“局部变量”,是因为这些变量只能用在ngFor
的循环体内。下面我们通过一个例子来了解这些局部变量的使用。
import { Component, OnInit } from '@angular/core'; interface Movie { title: string; director: string; cast: string; releaseDate: string; } @Component({ selector: 'app-local-vars', templateUrl: './local-vars.component.html', styleUrls: ['./local-vars.component.css'] }) export class LocalVarsComponent implements OnInit { title = 'Top 5 Movies'; movies: Movie[] = [ { title: 'Zootopia', director: 'Byron Howard, Rich Moore', cast: 'Idris Elba, Ginnifer Goodwin, Jason Bateman', releaseDate: 'March 4, 2016' }, { title: 'Batman v Superman: Dawn of Justice', director: 'Zack Snyder', cast: 'Ben Affleck, Henry Cavill, Amy Adams', releaseDate: 'March 25, 2016' }, { title: 'Captain American: Civil War', director: 'Anthony Russo, Joe Russo', cast: 'Scarlett Johansson, Elizabeth Olsen, Chris Evans', releaseDate: 'May 6, 2016' }, { title: 'X-Men: Apocalypse', director: 'Bryan Singer', cast: 'Jennifer Lawrence, Olivia Munn, Oscar Isaac', releaseDate: 'May 27, 2016' }, { title: 'Warcraft', director: 'Duncan Jones', cast: 'Travis Fimmel, Robert Kazinsky, Ben Foster', releaseDate: 'June 10, 2016' } ]; constructor() { } ngOnInit(): void { } }
<table> <thead> <tr> <th>Title</th> <th>Director</th> <th>Cast</th> <th>Release Date</th> </tr> </thead> <tbody> <tr *ngFor="let movie of movies; let i = index; let odd = odd; let even = even; let first = first; let last = last" [ngClass]="{ odd: odd, even: even, first: first, last: last }" > <td>{{ i }}</td> <td>{{ movie.title }}</td> <td>{{ movie.director }}</td> <td>{{ movie.cast }}</td> <td>{{ movie.releaseDate }}</td> </tr> </tbody> </table>
.even { background-color: azure; } .odd { background-color: floralwhite; } .first { background-color: yellowgreen; } .last { background-color: darkolivegreen; }
注意上面的代码,ngFor
里面使用let
语法,将局部变量进行赋值,然后我们就可以在ngFor
作用域中使用本地变量的名字。例如,我们将index
赋值给i
,然后就可以在内部使用这个i
了。对于奇数行和偶数行,我们则使用ngClass
指令,按照奇偶数的规则添加不同的 class。first
和last
的使用与此类似。最终运行结果如上面所示。
trackBy
现在我们已经学会如何使用ngFor
来渲染一组数据。例如上面我们见到的代码:
<ul> <li *ngFor="let movie of movies"> {{ movie.title }} - {{movie.director}} </li> </ul>
我们已经看到,Angular 为每一个 movie 对象创建一个 li 节点。然而,数据不会一成不变。我们可能新增电影、删除电影、修改电影的信息,Angular 需要追踪到这些变化,从而重新渲染模板。最简单的办法是,一旦检测到有数据变化,就把整个列表移除,然后重新生成一个新的列表替代。很明显,如果数据量很大,这一操作非常耗时,代价极大。对解决这一问题,Angular 使用对象标识符 object identity 来追踪数据到 DOM 节点的对应关系。因此,当你修改了数据的时候,Angular 就知道哪些数据发生了变化,只去修改发生变化的元素对应的 DOM 节点。到此为止,一切都很美好。但是,如果从服务器拉取了一遍新的数据呢?从服务器重新获取数据,即便是完全一样的数据,对象引用肯定和原始的不一样了。Angular 不会智能到去对比每个对象,它只要发现引用变化,就认为对象已经发生了变化。于是,Angular 销毁了旧的 DOM 节点,重新构建一遍新的。
为展示这一点,我们可以给组件添加一个refresh()
函数,用一个按钮去调用:
refresh(): void { this.movies = [ ...this.movies ]; }
从控制台的 HTML 面板就可以看出,整个<ul>
节点是被重新构建的。
为解决这一问题,Angular ngFor
提供了trackBy
属性。trackBy
是一个函数,返回能够标识每个元素的唯一 ID。ngFor
将使用trackBy
返回的这个唯一 ID 来追踪元素。这样一来,即便是我们刷新数据,只要数据的唯一 ID 保持不变,Angular 就不会重新渲染。
trackBy
函数有两个参数:索引位index
和当前元素item
;函数必须返回能够唯一标识元素的标识符。例如下面的代码,我们将title
当做这个唯一标识,也就是说,只要数据的title
不变,我们就认为这个数据是没有发生变化的:
trackByFn(index: number, item: Movie): string { return item.title; }
然后,我们将其赋值给ngFor
:
<ul> <li *ngFor="let movie of movies; trackBy: trackByFn;"> {{ movie.title }} - {{movie.director}} </li> </ul>
这样,即便我们刷新数据,只要title
保持不变,DOM 节点就不会重新渲染。
当然,trackBy
的定义是根据业务来的。如果我们同时需要根据title
和director
来标识一部电影——毕竟电影也有同名的问题——那么就应该使用两个属性:
trackByFn(index: number, item: Movie): string { return item.title + item.director; }
合理使用trackBy
,可以极大提升ngFor
的性能,优化我们的应用。这一点值得注意。