Списки: @for, track и @empty
Блок @for рисует список из массива, а параметр track подсказывает Angular, как не перерисовывать то, что не изменилось.
«Без track Angular перерисует весь список при любом чихе. С track — точечно тронет лишь то, что реально поменялось».
Отображение списков — ежедневная задача: товары, сообщения, строки таблицы. Новый блок @for заменил директиву *ngFor и сделал обязательным то, что раньше забывали — track. Это выражение, которое уникально опознаёт каждый элемент, чтобы фреймворк отслеживал их между перерисовками.
@Component({
selector: 'app-product-list',
standalone: true,
template: `
<ul>
@for (product of products; track product.id) {
<li>{{ product.title }} — {{ product.price }} ₽</li>
} @empty {
<li>Список пуст</li>
}
</ul>
`,
})
export class ProductListComponent {
products = [
{ id: 1, title: 'Кофемолка', price: 2990 },
{ id: 2, title: 'Чайник', price: 1990 },
];
}
Блок @empty — приятный бонус: он рендерится, когда массив пуст, избавляя от отдельной проверки @if. Внутри цикла доступны контекстные переменные: $index (номер), $first, $last, $even, $odd.
template: `
@for (item of items; track item.id; let i = $index) {
<div [class.even]="$even">{{ i + 1 }}. {{ item.name }}</div>
}
`
Как работает под капотом
Когда массив меняется, Angular берёт значение track у каждого элемента и сопоставляет старый список с новым. Совпал ключ — элемент DOM переиспользуется (обновляются только привязки). Не совпал — узел создаётся или удаляется. Без track по идентичности фреймворк не знал бы, какие узлы перерисовать, и пересоздавал бы весь список — это медленно и сбрасывает состояние (фокус, ввод).
старый: [id:1] [id:2] [id:3]
новый: [id:2] [id:3] [id:4]
|
сопоставление по track id
|
id:1 удалён -> узел убран
id:2,3 совпали -> узлы переиспользованы
id:4 новый -> узел создан
Запускаемая врезка: diff списка по ключу track
Покажем, что делает track «руками». «Попробуй сам ▶».
// сравнение списков по ключу id — как делает Angular @for track
function diffByKey(oldList, newList, key) {
const oldKeys = new Set(oldList.map(x => x[key]));
const newKeys = new Set(newList.map(x => x[key]));
const removed = oldList.filter(x => !newKeys.has(x[key]));
const added = newList.filter(x => !oldKeys.has(x[key]));
const reused = newList.filter(x => oldKeys.has(x[key]));
return { removed, added, reused };
}
const before = [{id:1},{id:2},{id:3}];
const after = [{id:2},{id:3},{id:4}];
const r = diffByKey(before, after, 'id');
console.log('удалить:', r.removed.map(x => x.id)); // [1]
console.log('создать:', r.added.map(x => x.id)); // [4]
console.log('переиспользовать:', r.reused.map(x => x.id)); // [2,3]
Частые ошибки
- Забыть track. В
@forон обязателен — без него код не скомпилируется. - Использовать
$indexкак track при перестановках. Если порядок меняется, ключом должен быть стабильный id, а не индекс. - Тяжёлые выражения в теле цикла. Они выполнятся для каждого элемента на каждой проверке.
Best practices
- Трекайте по уникальному и стабильному полю — обычно
idилиuuid. - Используйте
@emptyвместо отдельного@ifдля пустого состояния. - Не считайте, что элементы пересоздаются: при совпадении track сохраняется состояние DOM (фокус, ввод).
Итоги. @for ... track рисует и эффективно обновляет списки, @empty ловит пустоту, контекстные переменные дают индекс и позицию. Track — ключ к производительности. Дальше — слой сервисов и DI.
Закрепляем
Рендеринг списков — повседневная работа, и здесь главный герой это track. Без него Angular не смог бы понять, какой элемент DOM соответствует какому элементу данных после изменения массива, и был бы вынужден пересоздавать весь список целиком. Это не только медленно, но и сбрасывает состояние: фокус ввода, развёрнутые блоки, проигрываемое видео. С правильным track по стабильному идентификатору фреймворк точечно добавляет, удаляет и переставляет узлы, переиспользуя то, что не изменилось.
Выбирайте ключ для track вдумчиво. Идеален уникальный и стабильный идентификатор записи — обычно id или uuid из вашей модели данных. Использовать $index как ключ можно лишь для статичных списков, которые никогда не переупорядочиваются: при перестановке элементов индекс «съезжает», и Angular перепутает узлы, что приведёт к странным визуальным багам. Если в данных нет естественного идентификатора, всерьёз подумайте о том, чтобы его добавить — это окупится и в производительности, и в корректности.
| Конструкция | Назначение |
|---|---|
| @for (x of list; track x.id) | Цикл с ключом |
| @empty | Контент при пустом списке |
| $index, $first, $last | Контекстные переменные |
| $even, $odd | Чётность позиции |