Недоразумение при обнаружении изменений в Angular2 - с плункер

Я пытаюсь полностью понять обнаружение изменений с помощью Angular2 final.

Это включает:

  • Работа со стратегиями обнаружения изменений
  • Присоединение и отсоединение детектора изменений от компонента.

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

Мое общее представление об этом в целом правильное, но в некоторых ситуациях я немного теряюсь.


А вот и плункер: Angular2 Change detection Детская площадка

Краткое описание плункера:

Довольно просто:

  • Один родительский компонент, в котором вы можете редактировать один атрибут, который будет передан двум дочерним компонентам:
  • На дочернем элементе со стратегией обнаружения изменений, установленной на OnPush
  • На дочернем элементе со стратегией обнаружения изменений, установленной по умолчанию

Родительский атрибут может быть передан дочерним компонентам одним из следующих способов:

  • Изменение всего объекта атрибута и создание нового (кнопка «Изменить объект») (которая запускает обнаружение изменений в дочернем элементе OnPush)
  • Изменение членов внутри объекта атрибута (кнопка «Изменить содержимое») (которые не запускают обнаружение изменений в дочернем элементе OnPush)

К каждому дочернему компоненту можно прикрепить или отсоединить ChangeDetector. (кнопки "отсоединить ()" и "повторно прикрепить ()")

У дочернего элемента OnPush есть дополнительное внутреннее свойство, которое можно редактировать, и обнаружение изменений может быть применено явным образом (кнопка «detectChanges ()»).


Вот сценарии, при которых у меня появляется поведение, которое я не могу объяснить:

Сценарий 1:

  1. Отключить детектор изменений дочерних элементов OnPush и дочерних элементов по умолчанию (нажмите «отсоединить ()» на обоих компонентах)
  2. Отредактируйте родительский атрибут firstname и lastname
  3. Нажмите «Изменить объект», чтобы передать измененный атрибут дочерним элементам.

Ожидаемое поведение: я ожидаю, что ОБА дочерних элементов НЕ будут обновлены, потому что у них обоих отключен детектор изменений.

Текущее поведение: дочерний элемент по умолчанию не обновляется, но дочерний элемент OnPush обновляется. ПОЧЕМУ? Не следует, потому что его компакт-диск отсоединен ...

Сценарий 2:

  1. Отсоедините компакт-диск для компонента OnPush
  2. Измените его входное внутреннее значение и нажмите изменить внутреннее: ничего не происходит, потому что компакт-диск отсоединен, поэтому изменение не обнаружено ... ОК
  3. Нажмите detectChanges (): изменения будут обнаружены, и представление обновится. Все идет нормально.
  4. Еще раз отредактируйте ввод внутреннее значение и нажмите изменить внутреннее: снова ничего не происходит, потому что компакт-диск отсоединен, поэтому изменение не обнаружено .. ОК
  5. Отредактируйте родительский атрибут firstname и lastname.
  6. Нажмите «Изменить объект», чтобы передать измененный атрибут дочерним элементам.

Ожидаемое поведение: дочерние элементы OnPush НЕ должны обновляться ВООБЩЕ, потому что их компакт-диск отсоединен ... Компакт-диск не должен происходить в этом компоненте.

Текущее поведение: и значение, и внутренние значения обновляются, на этот компонент накладываются швы, как на полном компакт-диске.

  1. В последний раз отредактируйте ввод внутреннее значение и нажмите изменить внутреннее: изменение обнаружено, и внутреннее значение обновлено ...

Ожидаемое поведение: внутреннее значение НЕ следует обновлять, поскольку компакт-диск все еще отсоединен.

Текущее поведение: Обнаружено изменение внутреннего значения ... ПОЧЕМУ?


Выводы:

По результатам этих тестов я пришел к следующему выводу, который мне кажется странным:

  • Компонент со стратегией OnPush получает «обнаружено изменение» при изменении ввода, ДАЖЕ, ЕСЛИ их детектор изменений отключен.
  • Компонент со стратегией OnPush повторно подключает свой детектор изменений каждый раз, когда их ввод изменяется ...

Что вы думаете об этих выводах?

Можете ли вы лучше объяснить такое поведение?

Это ошибка или желаемое поведение?


person Clement    schedule 30.10.2016    source источник
comment
Не знаю, кто проголосовал против. Я думаю, что это довольно интересный вопрос, и проблема прекрасно объяснена.   -  person Günter Zöchbauer    schedule 30.10.2016
comment
Я не уверен, но думаю, что видел, как упоминалось, что detach() отключает детектор изменений для детей, но не сам компонент. Я сам еще не углублялся в обнаружение изменений.   -  person Günter Zöchbauer    schedule 30.10.2016
comment
В документе говорится: «Отсоединяет детектор изменений от дерева детекторов изменений». Прикрепление / отсоединение компакт-диска работает должным образом с ребенком, используя стратегию компакт-диска по умолчанию   -  person Clement    schedule 30.10.2016


Ответы (1)


Обновлять

Компонент со стратегией OnPush получает «обнаружение изменений» при изменении входных данных, ДАЖЕ ЕСЛИ их детектор изменений отключен.

Начиная с Angular 4.1.1 (2017-05-04) OnPush должен уважать detach()

https://github.com/angular/angular/commit/acf83b9

Старая версия

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

Нам следует знать о трех основных статусах changeDetection (cdMode):

1) CheckOnce - 0

CheckedOnce означает, что после вызова detectChanges режим детектора изменений станет Checked.

Класс AppView

detectChanges(throwOnChange: boolean): void {
  ...
  this.detectChangesInternal(throwOnChange);
  if (this.cdMode === ChangeDetectorStatus.CheckOnce) {
    this.cdMode = ChangeDetectorStatus.Checked; // <== this line
  }
  ...
}

2) Проверено - 1

Checked означает, что детектор изменений следует пропускать, пока его режим не изменится на CheckOnce.

3) Отдельно - 3

Detached означает, что поддерево детектора изменений не является частью основного дерева и его следует пропустить.

Вот места, где используется Detached

Класс AppView

Пропустить проверку содержимого

detectContentChildrenChanges(throwOnChange: boolean) {
  for (var i = 0; i < this.contentChildren.length; ++i) {
    var child = this.contentChildren[i];
    if (child.cdMode === ChangeDetectorStatus.Detached) continue; // <== this line
    child.detectChanges(throwOnChange);
  }
}

Пропустить проверку просмотра

detectViewChildrenChanges(throwOnChange: boolean) {
  for (var i = 0; i < this.viewChildren.length; ++i) {
    var child = this.viewChildren[i];
    if (child.cdMode === ChangeDetectorStatus.Detached) continue; // <== this line
    child.detectChanges(throwOnChange);
  }
}

Пропустить изменение cdMode на CheckOnce

markPathToRootAsCheckOnce(): void {
  let c: AppView<any> = this;
  while (isPresent(c) && c.cdMode !== ChangeDetectorStatus.Detached) { // <== this line
    if (c.cdMode === ChangeDetectorStatus.Checked) {
      c.cdMode = ChangeDetectorStatus.CheckOnce;
    }
    let parentEl =
        c.type === ViewType.COMPONENT ? c.declarationAppElement : c.viewContainerElement;
    c = isPresent(parentEl) ? parentEl.parentView : null;
  }
}

Примечание. markPathToRootAsCheckOnce работает во всех обработчиках событий вашего представления:

введите здесь описание изображения

Таким образом, если установить статус Detached, то ваше представление не изменится.

Тогда как работает OnPush стратегия

OnPush означает, что режим детектора изменений будет установлен на CheckOnce во время гидратации.

compiler / src / view_compiler / property_binder.ts

const directiveDetectChangesStmt = isOnPushComp ?
   new o.IfStmt(directiveDetectChangesExpr, [compileElement.appElement.prop('componentView')
           .callMethod('markAsCheckOnce', [])
           .toStmt()]) : directiveDetectChangesExpr.toStmt();

https://github.com/angular/angular/blob/2.1.2/modules/%40angular/compiler/src/view_compiler/property_binder.ts#L193-L197

Посмотрим, как это выглядит на вашем примере:

Родительская фабрика (AppComponent)

Введите здесь описание изображения

И снова вернемся к классу AppView:

markAsCheckOnce(): void { this.cdMode = ChangeDetectorStatus.CheckOnce; }

Сценарий 1

1) Отсоединить детектор изменений дочерних элементов OnPush и дочерних элементов по умолчанию (нажмите «отсоединить ()» на обоих компонентах)

OnPush.cdMode - Detached

3) Нажмите «Изменить объект», чтобы передать измененный атрибут дочерним элементам.

AppComponent.detectChanges
       ||
       \/
//if (self._OnPush_35_4.detectChangesInInputProps(self,self._el_35,throwOnChange)) {
//  self._appEl_35.componentView.markAsCheckOnce();
//}
OnPush.markAsCheckOnce
       ||
       \/
OnPush.cdMode - CheckOnce
       ||
       \/
OnPush.detectChanges
       ||
       \/
OnPush.cdMode - Checked

Поэтому OnPush.dectectChanges стреляет.

Вот вывод:

Компонент со стратегией OnPush получает «обнаружение изменений» при изменении входных данных, ДАЖЕ ЕСЛИ их детектор изменений отключен. Кроме того, он изменяет статус представления на CheckOnce.

Сценарий 2

1) Отсоедините компакт-диск от компонента OnPush

OnPush.cdMode - Detached

6) Нажмите «Изменить объект», чтобы передать измененный атрибут дочерним элементам.

See 3) from scenario 1 => OnPush.cdMode - Checked

7) В последний раз отредактируйте ввод внутреннего значения и нажмите кнопку изменить внутреннее: изменение обнаружено, и внутреннее значение обновлено ...

Как я уже упоминал выше, все обработчики событий включают markPathToRootAsCheckOnce. Так:

markPathToRootAsCheckOnce
        ||
        \/
OnPush.cdMode - CheckOnce
        ||
        \/
OnPush.detectChanges
        ||
        \/
OnPush.cdMode - Checked

Как видите, стратегия OnPush и ChangeDetector управляют одним свойством - cdMode.

Компонент со стратегией OnPush повторно подключает свой детектор изменений каждый раз, когда их ввод изменяется ...

В заключение хочу сказать, что, кажется, вы правы.

person yurzui    schedule 30.10.2016
comment
Отличное объяснение. Ответ, который объясняет то же самое в сообщении Бена Наделя: bennadel.com/blog/. Это относится к следующей проблеме github для этой точной проблемы: github.com/angular/angular/issues/9720 - person KwintenP; 30.10.2016
comment
Отличное объяснение. Кажется, отсутствует изображение. - person Günter Zöchbauer; 30.10.2016
comment
Отлично, спасибо за этот отличный ответ и за связанную с ним проблему с github! Теперь это совершенно ясно. - person Clement; 31.10.2016