Angular 学习之路 13 – ngFor

上一章我们介绍了ngIf,这是一个条件选择的指令。与此类似,Angular 还提供了用于循环的指令:ngForngFor指令遍历一个数据集合,;例如数组、列表等,然后在 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 itemsitems是遍历的集合,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>

ngFor List Demo

首先,我们定义了一个interface Movie,用于定义数据。TypeScript 的interface与 Java 的类似,都是用于定义一种规范,不同之处在于,TypeScript 的interface可以定义属性,用于规范数据的类型。在组件类中,我们利用前面定义的Movie类型,以数组的形式给出了 2016 年的几部著名电影的信息。接下来的 HTML 代码中,我们使用ngFor遍历这个数组,利用字符串绑定将数据输出到页面。

利用浏览器的审查元素功能,我们可以看到,Angular 是如何生成li标签的:

ngFor <ul>

下面我们看一个更复杂的例子,嵌套数组的遍历:

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>
ngFor Table Demo

与前面的例子类似,我们同样定义了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 Local Vars

注意上面的代码,ngFor里面使用let语法,将局部变量进行赋值,然后我们就可以在ngFor作用域中使用本地变量的名字。例如,我们将index赋值给i,然后就可以在内部使用这个i了。对于奇数行和偶数行,我们则使用ngClass指令,按照奇偶数的规则添加不同的 class。firstlast的使用与此类似。最终运行结果如上面所示。

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的定义是根据业务来的。如果我们同时需要根据titledirector来标识一部电影——毕竟电影也有同名的问题——那么就应该使用两个属性:

trackByFn(index: number, item: Movie): string {
  return item.title + item.director;
}

合理使用trackBy,可以极大提升ngFor的性能,优化我们的应用。这一点值得注意。

Leave a Reply