angular
March 29

Отладка changeDetection в Angular с использованием Chrome DevTools

Иногда changeDetection в Angular работает не так как хотелось бы и может срабатывать когда мы этого не ожидаем. Найти причину порой может оказаться сложно. Найти корень проблемы может оказаться сложно. В Angular цикл обнаружения изменений может быть запущен по различным событиям браузера, связанным с взаимодействием с пользовательским интерфейсом, сетевыми запросами или таймерами. В реальных приложениях эти события переплетаются таким образом, что очень сложно определить причину конкретного запуска обнаружения изменений. В этом случае отладочные возможности браузера приходят очень кстати.

В этом материале будет показано, как использовать стек вызовов (callstack), точки логирования (logpoints), фильтрацию и локальные переопределения для отслеживания причины неожиданных запусков обнаружения изменений.

Начнем с самого простого сценария. У нас есть элемент пользовательского интерфейса, отрисованный на странице. Мы замечаем, что при наведении курсора на этот элемент происходит обнаружение изменений. Это распространенный сценарий для сторонних библиотек, которые присоединяют обработчики событий к элементам пользовательского интерфейса, о которых мы не знаем:

@Component({
  selector: 'app-root',
  template: `
  <div (mouseover)="0">Just a plain div</div>
  <child-cmp></child-cmp>
 `,
})
export class AppComponent {}
 
@Component({
  selector: 'child-cmp',
  template: `
    <div>Changes detected: {{n}}</div>
    <button (click)="fetch()">Fetch</button>
  `,
})
export class ChildComponent {
  n = String(Date.now()).slice(-4);
 
  ngDoCheck() {
    this.n = String(Date.now()).slice(-4);
  }
}

При выполнении компонента он будет выглядеть примерно так:

Предположим, что мы не знаем о подписчике события mouseover, который запускает цепочку changeDetection. Попробуем выяснить причину используя Chrome DevTools.

Первый шаг - добавим точку останова внутри функции компонента Child Component.

Когда обнаружение изменений приостановится, мы увидим такую картину

Используя глобальный поиск найти компонент проще всего. Достаточно вбить в поиск весь исходный код компонента:

@Component({
  selector: 'child-cmp',
 ...
})
export class ChildComponent {
  constructor() {
    console.log(this)
  }
}


Так же можно перейти к функции из консоли. Для этого достаточно вывести в консоль this, а затем найти в прототипах конструктор и перейти к объявлению через контекстное меню "Show function definition":

Функция шаблона, созданная компилятором Angular, должна находиться непосредственно под классом компонента:

Далее переключимся на боковую панель и проанализируем стек вызовов. Наведем курсор на элемент, чтобы сработал changeDetection и выполнение компонента остановилось. В данном случае видно, что на этот раз Angular просто запускает проверку изменений на уровне всего приложения, которая началась с функции tick:

sourcemaps для typescript должны быть отключены

Если это был случай локальной проверки изменений, мы бы видели функцию в стеке вызовов, которая вызвала проверку изменений, а не метод tick. Это могло бы выглядеть так:

Попробуем узнать, что именно вызвало tick - для этого нам пригодится функция onInvokeTask. Этот callback выполняется Zone.js, когда задача завершена.

Просмотрим контекст выполнения, щелкнув по функции в стеке вызовов, и изучим соответствующее событие, как показано ниже:

Отсюда явно видно, что в аргументе task хранится событие которое привело к срабатыванию changeDetection.

Использование logpoints

В реальных приложениях будут сотни и тысячи событий, которые происходят почти одновременно и запускают changeDetection. Попытка определить одно отдельное событие, приостанавливая выполнение всего приложения, может быть неосуществимой. В этом случае намного эффективнее использовать точки журнала.

Чтобы зарегистрировать все события, которые проходят через механизм Zone.js. Добавим точку журнала в метод runTask следующим образом:

Таска содержит следующую информацию:

  • source — API который запросил задачу
  • target — цель события
  • eventName — нативное название события

Наведем курсор на элемент и получим вывод:

logpoint показывает, что имя события - mouseover, а целью события является div.

Так же можно зарегистрировать в логпоинтах вызов метода tick.

Теперь можно наглядно увидеть, в какой момент запускается цикл обнаружения изменений.

Иногда обнаружение изменений запускается макрозадачой, у которой есть промежуток времени между ее началом и окончанием. Хорошим примером являются события сети или таймера. Для событий этого типа, чтобы выяснить причину вызова changeDetection, нам также необходимо отслеживать часть планирования.

Отслеживание источника задачи

Немного изменим пользовательский интерфейс. Мы введем кнопку, которая будет запускать сетевой запрос:

Вот как выглядит реализация:

@Component({
  selector: 'child-cmp',
 template: `
    <div>Records count: {{recordsCount}}</div>
    <button (click)="getTodos()">Fetch</button>
  `,
})
export class ChildComponent {
  getTodos() {
  fetch('https://jsonplaceholder.typicode.com/todos')
     .then(r => r.json())
     .then(c => this.recordsCount = c.length);
  }
}

В этом примере zone.js потребуется обработать две задачи - сетевой запрос и несколько promise. Разберем как в таком случае работает планировщик задач.

Планирование задач происходит внутри метода scheduleTask. Именно здесь мы добавим точку входа для вывода свойств источника и типа события. Созданные ранее логпоинты не отключаем.

При клике на кнопку fetch произойдет вызов цепочки событий:

Мы видим, что запускается обработчик кликов и запланирована куча промисов. Однако для метода fetch API не запланирована макрозадача. Это потому, что zone.js не планирует задачу для самого сетевого запроса, а вместо этого просто немедленно вызывает API браузера. Вызов API затем преобразуется в промис, так что в итоге мы получаем 3 обещания - 2, которые исходят из кода нашего приложения, и одно, которое обертывает макрозадачу извлечения из zone.js.

Хотя мы видим, что промисы запланированы, это не помогает нам определить его причину. Для этого нам нужно поместить точку останова в функцию scheduleMicroTask и изучить стек вызовов:

В одном из событий дебагера мы сможем отловить событие обработчика click.

Console.trace

Еще один подход заключается в использовании console.trace

Клик по кнопке выведет в консоль следующее:

Здесь мы снова видим, что промис был получен из обработчика события click.

Вывод в консоли можно сделать удобнее используя Group Collapsed API.

console.groupCollapsed(`schedule: ${task.type}, ${task.source}, ${task.type}`),
 console.trace(),
 console.groupEnd()

Достаточно просто добавить его при создании логпоинта.

Вывод будет выглядеть следующим образом. Сразу понятна последовательность добавления задач в очередь, и при необходимости с каждой можно ознакомиться:

Из такого вывода так же можно определить, что событие было порождено кликом:

Local Overrides API

Альтернативой использованию логпоинтов для выполнения инструкций может послужить local override API, который позволит нам поместить эти выражения непосредственно в исходники Вот как мы могли бы это сделать:

Эта функциональность для переопределения исходных текстов может быть очень удобна во многих ситуациях во время отладки. Но работает только до перезагрузки страницы, после чего изменения будут сброшены.

Это вольный перевод материала
Код здесь