11 — Жизненный цикл и afterRender на пальцах
Цель: понять, в каком порядке Angular зовёт хуки компонента (ngOnInit,
ngAfterViewInit и т.д.), почему именно в таком, и что такое afterEveryRender /
afterNextRender с их «фазами». Точные file:line — в соседнем vault'е notes/v21/.
Главная идея
Компонент — не просто «создался и живёт». У него есть точки в жизни, в которые Angular даёт тебе вклиниться: «вот сейчас я тебя создал», «вот сейчас обновил входы», «вот сейчас отрисовал детей», «вот сейчас удаляю». Эти точки и есть хуки.
Аналогия: стройка дома.
ngOnChanges — привезли стройматериалы (входы), сверяем что изменилось
ngOnInit — фундамент залит, дом «официально начат» (один раз)
ngDoCheck — прораб обходит стройку на каждой проверке
ngAfterContentInit— заехали жильцы со своей мебелью (спроецированный контент готов)
ngAfterViewInit — дом достроен целиком, включая все комнаты-дети (один раз)
ngOnDestroy — дом сносят, выносим вещи (отписки, очистка)
Два прохода — два набора хуков
Помни из 04-runtime-cd: есть create-проход (один раз, строим) и update-проход (много раз, обновляем). Хуки делятся ровно по этому признаку:
Init-хуки (один раз, на create): Check-хуки (каждый update):
ngOnInit ngDoCheck
ngAfterContentInit ngAfterContentChecked
ngAfterViewInit ngAfterViewChecked
ngOnChanges — особый: зовётся и на create (первые входы), и на update (если входы
поменялись).
Точный порядок за один проход
Запомни мнемонику: сверху вниз приходят входы, снизу вверх готовятся дети.
CREATE (первый раз):
1. ngOnChanges ← если есть @Input / input()
2. ngOnInit ← один раз, «я родился»
3. ngDoCheck
↓ выполняется шаблон (создаются дети)
4. ngAfterContentInit ← спроецированный <ng-content> на месте
5. ngAfterViewInit ← все дочерние компоненты уже прошли свой create
(поэтому ViewChild доступен именно здесь!)
UPDATE (каждая проверка):
1. ngOnChanges ← если входы изменились
2. ngDoCheck
↓ обновляется шаблон
3. ngAfterContentChecked
4. ngAfterViewChecked
5. afterEveryRender / afterNextRender ← после ВСЕГО, когда DOM уже отрисован
Почему ngOnInit до детей, а ngAfterViewInit после? Потому что входы текут
сверху вниз (родитель → ребёнок), а готовность view собирается снизу вверх:
родитель «готов целиком» только когда готовы все его дети. Поэтому @ViewChild
гарантированно есть в ngAfterViewInit, а не в ngOnInit (в ngOnInit детей ещё нет).
ngOnChanges и SimpleChanges
ngOnChanges срабатывает только для входов (@Input / input()), и только когда
значение реально пришло/изменилось. Под капотом компилятор оборачивает установку
входов так, чтобы собрать объект SimpleChanges:
ngOnChanges(changes: SimpleChanges) {
// changes.title.previousValue / currentValue / firstChange
if (changes['title']) { ... }
}
Важно: меняешь поле внутри компонента (this.x = 5) — ngOnChanges НЕ позовётся,
он только про входы снаружи.
ngOnDestroy — уборка за собой
Когда view уничтожается (@if стало false, элемент @for убрали, ушли со
страницы), Angular зовёт ngOnDestroy. Это твоё место, чтобы:
- отписаться от ручных
subscribe()(если не используешьtakeUntilDestroyed/async); - очистить таймеры, слушатели, веб-сокеты.
Сигнальные effect() и подписки через DestroyRef чистятся сами — для них
ngOnDestroy не нужен.
afterEveryRender / afterNextRender — «когда DOM уже на экране»
Хуки выше работают до того, как браузер реально отрисовал пиксели. Если тебе нужно потрогать настоящий DOM (измерить размер, проинициализировать стороннюю библиотеку графиков, поставить фокус) — нужны эти две функции:
constructor() {
afterNextRender(() => { // один раз после ближайшего рендера
this.chart = new Chart(this.canvas.nativeElement);
});
afterEveryRender(() => { // каждый раз после рендера
console.log('перерисовались');
});
}
afterNextRender— сработает один раз, после первого же завершённого рендера. Идеально для инициализации (какngAfterViewInit, но гарантирует, что DOM реально в документе).afterEveryRender— срабатывает после каждого рендера. Осторожно: легко словить лишние срабатывания, не делай тут тяжёлого.
Важно: они НЕ запускаются на сервере (SSR) — только в браузере. Это специально: измерять DOM на сервере нечего.
Фазы after-render-хуков: earlyRead → write → mixedReadWrite → read
Вот то самое уточнение. У afterEveryRender можно указать фазу. Зачем вообще фазы?
Проблема называется layout thrashing («дёргание лейаута»). Если в коде чередовать «прочитал размер из DOM → записал в DOM → снова прочитал → снова записал», браузер вынужден пересчитывать лейаут на каждом чтении — это тормозит. Решение: сгруппировать все чтения вместе, все записи вместе. Фазы — это и есть способ сказать Angular, к какой группе относится твой колбэк, чтобы он выполнил их в правильном порядке.
Порядок выполнения фаз за один рендер:
1. earlyRead — прочитать DOM ДО записей (если запись зависит от размера)
2. write — только писать в DOM (ничего не читать!)
3. mixedReadWrite — и читаю, и пишу вперемешку ← ДЕФОЛТ, но самый медленный
4. read — только читать DOM после всех записей
Та формулировка из доков — «эффект, который вызывается после рендера, в фазе
mixedReadWrite» — описывает поведение по умолчанию: если ты просто передал
функцию без указания фазы, она попадает в mixedReadWrite. Это безопасно (можно всё),
но наименее оптимально, потому что Angular не может сгруппировать твои чтения и
записи с чужими — фаза «и туда и сюда».
// по умолчанию = mixedReadWrite (работает, но не оптимально)
afterEveryRender(() => { ... });
// оптимально — явно разделить чтение и запись:
afterEveryRender({
read: () => { this.height = el.offsetHeight; }, // только читаю
write: () => { el.style.transform = '...'; }, // только пишу
});
Правило на практике: если можешь — раздели на read и write, не сваливай всё в
дефолтный mixedReadWrite. Тогда браузер сделает один пересчёт лейаута, а не десять.
Официальная рекомендация: предпочитай read и write, а earlyRead и
mixedReadWrite — только когда иначе никак.
afterRenderEffect — «фазы + реактивность»
afterNextRender / afterEveryRender — это простые колбэки: положил функцию,
она вызвалась после рендера. afterRenderEffect (@angular/core) — это то же
самое, но с суперсилой effect(): см. реактивность в 04-runtime-cd.
Два ключевых отличия:
1. Реактивность — перезапускается сам. Обычный afterEveryRender срабатывает на
каждый рендер. afterRenderEffect — «грязный» только когда изменился сигнал,
который он прочитал. То есть он перезапустится, лишь если поменялись его реальные
зависимости (плюс минимум один раз). Это как effect(), но гарантированно после
отрисовки DOM.
size = signal({ w: 0, h: 0 });
constructor() {
afterRenderEffect({
write: () => {
const s = this.size(); // прочитали сигнал → это зависимость
this.el.nativeElement.style.width = s.w + 'px';
},
});
// перезапустится ТОЛЬКО когда size() изменится, а не на каждый рендер
}
2. Значения текут между фазами как Signal. Те же 4 фазы (earlyRead → write → mixedReadWrite → read), но теперь это отдельные эффекты в цепочке: первая фаза
не получает аргументов, а каждая следующая получает результат предыдущей как
Signal. Это и есть «правильный» способ сделать «измерил → записал → проверил» без
дёргания лейаута:
afterRenderEffect({
earlyRead: () => this.el.nativeElement.offsetHeight, // вернули число
write: (height) => { // height: Signal<number>
this.el.nativeElement.style.transform = `translateY(${height()}px)`;
return height(); // передаём дальше
},
read: (shifted) => { // shifted: Signal<number>
console.log('после сдвига', shifted());
},
});
Поток: earlyRead всё прочитал (но не писал) → его результат пришёл в write как
сигнал → write всё записал (но не читал) → результат в read. Браузер делает один
проход лейаута, а не чередует чтение/запись.
Когда что брать:
- разово инициализировать (график, фокус) →
afterNextRender({ write }); - реакция на изменение сигнала с доступом к DOM, без layout thrashing →
afterRenderEffectс разделением на фазы; - просто «что-то после каждого рендера» (логи, дёшево) →
afterEveryRender.
Осторожно: на момент колбэка компонент может быть ещё не гидратирован (см. SSR) — не полагайся слепо на готовность DOM при гидратации.
Карта порядка
СОЗДАНИЕ ОБНОВЛЕНИЕ
входы↓ ngOnChanges → ngOnInit → ngDoCheck ngOnChanges → ngDoCheck
↓ шаблон/дети ↓ шаблон/дети
контент ngAfterContentInit ngAfterContentChecked
дети↑ ngAfterViewInit ngAfterViewChecked
↓
afterEveryRender (earlyRead→write→mixedReadWrite→read)
... и в конце жизни: ngOnDestroy → очистка слушателей/эффектов
Итог
- Init-хуки — один раз (create), Check-хуки — каждый раз (update).
- Входы текут сверху вниз (
ngOnChanges/ngOnInitрано), готовность собирается снизу вверх (ngAfterViewInitпоздно) — поэтому@ViewChildесть только после view. this.x=5внутри не зовётngOnChanges— он только про входы.- Трогать реальный DOM — через
afterNextRender(один раз) /afterEveryRender(каждый раз), и только в браузере. - Фазы (
read/write) нужны, чтобы не дёргать лейаут; дефолтmixedReadWrite— самый простой, но самый медленный, лучше разделять.
Дальше → 12-queries: как @ViewChild / viewChild() реально находят элемент.