Angular — все, что вам нужно знать о `ExpressionChangedAfterItHasBeenCheckedError`

внешний интерфейс JavaScript Google Angular.js
Angular — все, что вам нужно знать о `ExpressionChangedAfterItHasBeenCheckedError`

Перевод статьи получен с согласия автора оригинала.Оригинальная ссылка:link

текст

В последнее время в stackoverflow я всегда вижу, что когда кто-то спрашивает об использовании Angular, они сообщаютExpressionChangedAfterItHasBeenCheckedErrorошибка. Большинство людей, задающих вопрос, не понимают механизм мониторинга изменений Angular или зачем нужно это сообщение об ошибке. Некоторые разработчики даже думают, что это ошибка, но на самом деле это не ошибка.

В этой статье подробно объясняется, что вызывает эту ошибку и как ее обнаружить, а также показаны сценарии, в которых ошибка часто возникает, и дается несколько возможных решений. В последней главе объясняется, зачем Angular нужен этот механизм мониторинга.

О мониторинге изменений

Каждое приложение Angular представлено в виде дерева компонентов. Angular выполняет действия над каждым компонентом в следующем порядке на этапе обнаружения изменений (List1):

Есть несколько других операций, которые выполняются на этапе мониторинга изменений, о которых я подробно расскажу в этом посте:Everything you need to know about change detection in Angular

После каждой операции Angular сохраняет значения, связанные с этой операцией, которые хранятся в представлении компонента.oldValuesв свойствах. (в режиме разработки) После того, как все компоненты завершили мониторинг изменений, Angular запустит следующийПроцесс мониторинга, второй процесс мониторинга не будет снова выполнять процесс мониторинга изменений, указанный выше, а будет сравнивать значение, сохраненное в предыдущем цикле мониторинга изменений (в oldValues), со значением текущего процесса мониторинга (List2):

  • Проверяет, чтобы значения, переданные дочернему компоненту (oldValues), соответствовали значениям (instance.value), которые текущий компонент должен использовать для обновления
  • Убедитесь, что значения, используемые для обновления элементов DOM (oldValues), соответствуют значениям, используемым в настоящее время для обновления этих компонентов (instance.value)
  • Выполните одинаковую проверку для всех дочерних компонентов.

Примечание: эти дополнительные проверки (List2) происходит только в режиме разработки, и я объясню почему в следующей главе.

Далее рассмотрим пример. Предположим, у вас есть родительский компонент A и дочерний компонент B, и в компоненте A есть два свойства:nameа такжеtext, используемый в шаблоне компонента AnameАтрибуты:

template: '<span>{{name}}</span>'

Затем добавьте компонент B в шаблон и привяжите его к входу компонента B через свойство input.textАтрибуты:

@Component({
    selector: 'a-comp',
    template: `
        <span>{{name}}</span>
        <b-comp [text]="text"></b-comp>
    `
})
export class AComponent {
    name = 'I am A component';
    text = 'A message for the child component`;

Так что же происходит после того, как Angular запустит мониторинг изменений? (List1) мониторинг изменений начнется с компонента A, первый шаг будетtextв выраженииA message for the child componentПередайте компоненту B и сохраните это значение в представлении:

view.oldValues[0] = 'A message for the child component';

Затем дело доходит до второго шага в списке мониторинга изменений, вызывая соответствующую функцию жизненного цикла.

Далее выполните третий шаг,{{name}}Выражение разрешаетсяI am A componentтекст. Обновите проанализированное значение в DOM и сохраните его вoldValues:

view.oldValues[1] = 'I am A component';

Наконец, Angular делает то же самое для компонента B (List1), как только компонент B завершит вышеуказанные операции, цикл мониторинга изменений будет завершен.

Если Angular запущен в режиме разработки, будет выполнен другой процесс мониторинга (List2).textЗначение свойства при передаче компоненту B равноA message for the child componentи депозитoldValues, теперь представьте, что компонент А будет после этогоtextЗначение обновляется доupdated text. потомList2Первым шагом будет проверкаtextИзменилось ли свойство:

AComponentView.instance.text === view.oldValues[0]; // false
'updated text' === 'A message for the child component'; // false

В этот момент Angular должен выдать эту ошибку

ExpressionChangedAfterItHasBeenCheckedError.

Точно так же, если обновление уже было отображено в DOM и существуетoldValuesсерединаnameсвойство, также выдает ту же ошибку

AComponentView.instance.name === view.oldValues[1]; // false
'updated name' === 'I am A component'; // false

Теперь вам может быть интересно, как можно изменить эти значения? Давайте посмотрим вниз.

Причина изменения данных

Виновниками обычно являются подкомпоненты или директивы Давайте рассмотрим простой случай. Сначала я воссоздам сценарий на простейшем возможном примере, а позже приведу реальный пример. Всем известно, что родительские компоненты могут использовать дочерние компоненты или инструкции.Вот родительский компонент A, дочерний компонент B, и компонент B имеет свойство привязкиtext. Мы будем использовать подкомпонентngOnInit(На данный момент данные привязаны) Обновление в хуке жизненного циклаtextАтрибуты:

export class BComponent {
    @Input() text;

    constructor(private parent: AppComponent) {}

    ngOnInit() {
        this.parent.text = 'updated text';
    }
}

Видим ожидаемую ошибку:

Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'A message for the child component'. Current value: 'updated text'.

Теперь мы используем шаблон родительского компонентаnameсвойства делают то же самое:

ngOnInit() {
    this.parent.name = 'updated name';
}

В это время программа не сообщает об ошибке, почему так происходит?

Если вы внимательно посмотрите на мониторинг изменений (List1) в порядке выполнения вы обнаружите, что дочерний компонентngOnInitбудет вызываться перед обновлением DOM текущего компонента (данные были изменены до того, как были записаны oldValues), поэтому в приведенном выше примере изменениеnameсвойство не сообщает об ошибке. Нам нужен хук после обновления значений в DOM для экспериментов,ngAfterViewInitхороший выбор:

export class BComponent {
    @Input() text;

    constructor(private parent: AppComponent) {}

    ngAfterViewInit() {
        this.parent.name = 'updated name';
    }
}

Снова получаем ожидаемую ошибку:

AppComponent.ngfactory.js:8 ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'I am A component'. Current value: 'updated name'.

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

Теперь давайте рассмотрим некоторые реальные случаи.

общий сервис

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

Синхронизированная трансляция событий

пример:plunker. В этом приложении родительский элемент прослушивает событие, транслируемое дочерним элементом, что приводит к обновлению свойства родительского элемента, которое, в свою очередь, используется для привязки Input дочернего элемента. Это также косвенно обновляет свойства родительского элемента.

Создание динамического компонента

Этот режим немного отличается от двух предыдущих режимов, которые обаList2Первый шаг в обнаружении выброшенных ошибок, и этот режим обнаруживается обновлениями DOM (List2Шаг 2) выдает ошибку. пример:plunker. Родительский компонент в этом приложенииngAfterViewInitПодкомпоненты динамически добавляются в жизненном цикле.Этот жизненный цикл происходит после первоначального обновления DOM текущего компонента, и добавление подкомпонентов изменит структуру DOM, поэтому значения, используемые в двух DOM, будут разными (при условии, что подкомпоненты имеют новую ссылку на значение), поэтому возникает ошибка.

возможное решение

Если вы внимательно посмотрите на последнее предложение сообщения об ошибке:

Выражение изменилось после проверки Предыдущее значение:… Было ли оно создано в хуке обнаружения изменений?

В случае динамически создаваемых компонентов лучшим решением этой проблемы является изменение хука жизненного цикла, в котором создается компонент. Например, процесс динамического создания компонентов из предыдущей главы можно перенести вngOnInitсередина. Хотя в документации указаноViewChildrenтолько вngAfterViewInitОн приобретается позже, но дочерние компоненты заполняются при создании представления, поэтому его можно получить заранее.ViewChildren.

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

Асинхронное обновление

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

export class BComponent {
    name = 'I am B component';
    @Input() text;

    constructor(private parent: AppComponent) {}

    ngOnInit() {
        setTimeout(() => {
            this.parent.text = 'updated text';
        });
    }

    ngAfterViewInit() {
        setTimeout(() => {
            this.parent.name = 'updated name';
        });
    }
}

действительно ошибок не выдаетsetTimeoutДобавьте функцию в очередь макрозадач, и функция будет вызвана в следующем цикле виртуальной машины. Вы также можете использовать обещание вthenОбратный вызов добавляет функцию в текущий цикл виртуальной машины после выполнения другого синхронного кода:

Promise.resolve(null).then(() => this.parent.name = 'updated name');

Promise.thenОн не будет помещен в макрозадачу, но создаст микрозадачу. Очередь микрозадач будет выполнена после выполнения всего кода синхронизации в текущем цикле, поэтому обновление свойств произойдет после шага проверки. Чтобы узнать больше об использовании микро- и макрозадач в Angular, см. эту статью:Я реконструировал Zones (zone.js) и вот что я нашел.

ДатьEventEmitterпройти одинtrueЧтобы сделать отправку события асинхронной:

new EventEmitter(true);

Обязательный мониторинг изменений

Другое решение — принудительно запустить цикл мониторинга изменений между первой и второй проверкой родительского компонента A. Лучшее место для запуска мониторинга принудительных изменений — этоngAfterViewInitВ жизненном цикле в это время были выполнены процессы всех подкомпонентов, поэтому не имеет значения, где изменяются свойства родительского компонента в любой предыдущей позиции:

export class AppComponent {
    name = 'I am A component';
    text = 'A message for the child component';

    constructor(private cd: ChangeDetectorRef) {
    }

    ngAfterViewInit() {
        this.cd.detectChanges();
    }

Что ж, ошибки нет, вроде бы удалось запустить программу счастливо. На самом деле, здесь есть проблема: когда в родительском компоненте A запускается недавно добавленный мониторинг изменений, Anuglar также запускает мониторинг изменений для всех дочерних компонентов, поэтому родительский компонент может снова обновиться.

Зачем нужен второй цикл мониторинга

Angular обеспечивает однонаправленный поток данных сверху вниз, после того как родительский элемент завершает обнаружение изменений, он не позволяет внутренним дочерним компонентам изменять свойства родительского компонента до второго обнаружения изменений. Это гарантирует стабильность дерева компонентов после первого мониторинга изменений. Если в цикле мониторинга происходят изменения свойств, из-за которых пользователи, зависящие от этих свойств, синхронно обновляют изменения, дерево компонентов становится нестабильным. В приведенном выше примере дочерний компонент B зависит от родительского компонента.textАтрибуты, всякий раз, когда значение атрибута изменяется, дерево компонентов находится в нестабильном состоянии, пока эти изменения не будут переданы компоненту B. Это также отражено в отношениях между DOM и атрибутами: DOM действует как пользователь этих атрибутов, а затем отображает эти атрибуты в интерфейсе пользовательского интерфейса. Если некоторые свойства интерфейса не обновляются синхронно, пользователь увидит неправильный интерфейс.

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

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

Интересно, что AngularJS (Angular 1.x) не использует односторонний поток данных, чтобы в значительной степени обеспечить стабильность дерева компонентов. Но мы часто видим пресловутую ошибку10 $digest() iterations reached. Aborting!. Просто погуглите, и вы найдете множество вопросов об этой ошибке.

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

Первый перевод немного дерганый, пожалуйста, укажите на ошибки.