錯誤百科

追蹤表達式導致 DOM 結構重新建立

在 @for 迴圈中指定的身分追蹤表達式,導致對應到_所有_項目的 DOM 被重新建立。這是非常昂貴的操作,常發生在處理不可變資料結構時。例如:

@Component({
  template: `
    <button (click)="toggleAllDone()">All done!</button>
    <ul>
      @for (todo of todos; track todo) {
        <li>{{ todo.task }}</li>
      }
    </ul>
  `,
})
export class App {
  todos = [
    {id: 0, task: 'understand trackBy', done: false},
    {id: 1, task: 'use proper tracking expression', done: false},
  ];

  toggleAllDone() {
    this.todos = this.todos.map((todo) => ({...todo, done: true}));
  }
}

在提供的範例中,切換項目的「done」狀態後,整個清單連同所有檢視(DOM 節點、Angular 指令、元件、查詢等)都會被重新建立(!)。在這裡,對 done 屬性做一次相對便宜的繫結更新就足夠了。

除了高效能代價之外,重新建立 DOM 樹也會導致 DOM 元素中的狀態遺失(例如:焦點、文字選取、在 iframe 中載入的網站等)。

修正方式

變更追蹤表達式,讓它能唯一識別集合中的項目,不受其物件身分影響。在討論的範例中,正確的追蹤表達式會使用唯一的 id 屬性(item.id):

@Component({
  template: `
    <button (click)="toggleAllDone()">All done!</button>
    <ul>
      @for (todo of todos; track todo.id) {
        <li>{{ todo.task }}</li>
      }
    </ul>
  `,
})
export class App {
  todos = [
    {id: 0, task: 'understand trackBy', done: false},
    {id: 1, task: 'use proper tracking expression', done: false},
  ];

  toggleAllDone() {
    this.todos = this.todos.map((todo) => ({...todo, done: true}));
  }
}