angular
April 1

Change Detection и деревья компонентов в Angular

Angular-приложение проще всего представить как дерево компонентов. Под капотом Angular использует низкоуровневую абстракцию, называемую View. Структура View определяется интерфейсом LView. LView хранит всю необходимую информацию для обработки инструкций по мере их вызова из шаблона.

У каждого компонента и вложенного шаблона есть свой соответствующий LView. По сути это группа элементов, которые создаются и уничтожаются вместе. Все операции, такие как проверка свойств и обновление DOM, выполняются над View, поэтому верно утверждать, что Angular представляет собой дерево View, в то время как компонент можно описать как высокоуровневое понятие View. Мы можем обращаться к View, созданным для компонентов, как к представлениям компонентов, чтобы отличить их от встроенных представлений, созданных с использованием ViewContainerRef с использованием ссылок на шаблоны, например, элементы ng-template.

export const PARENT = 3;
export const NEXT = 4;
export const CHILD_HEAD = 13;
export const CHILD_TAIL = 14;
 
export interface LView {
  [CHILD_HEAD]: LView|LContainer|null;
  [CHILD_TAIL]: LView|LContainer|null;
  [PARENT]: LView|LContainer|null;
  [NEXT]: LView|LContainer|null;
}

Для обхода дерева представлений Angular использует эти утилиты обхода.

Angular также реализует структуру данных TView, которая содержит статические данные для LView. TView общий для всех LView одного типа. Это означает, что каждый экземпляр определенного компонента имеет свой собственный экземпляр LView, но все они ссылаются на один и тот же экземпляр TView.

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

export const enum TViewType {
  Root = 0,
  Component = 1,
  Embedded = 2,
}

Componentи Embedded понятны сами по себе. Root - это специальный тип представлений, который Angular использует для загрузки компонентов верхнего уровня. Оно используется совместно с LView, который берет существующий узел DOM, не принадлежащий Angular, и оборачивает его в LView, чтобы в него можно было загружать другие компоненты.

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

Дерево обнаружения изменений

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

Angular хранит верхние уровни таких деревьев в свойстве _views объекта ApplicationRef. Эти деревья называются деревьями changeDetection, потому что они обходятся, когда Angular запускает обнаружение изменений. Метод tick, который запускает обнаружение изменений, перебирает каждое дерево в _views и выполняет проверку для каждого представления, вызывая метод detectChanges:

@Injectable({ providedIn: 'root' })
export class ApplicationRef {
  tick(): void {
    try {
      this._runningTick = true;
      for (let view of this._views) {
        view.detectChanges();
      }
      if (typeof ngDevMode === 'undefined' || ngDevMode) {
        for (let view of this._views) {
          view.checkNoChanges();
        }
      }
    } catch (e) { ... } finally { ... }
  }
}

Так же метод tick вызывает метод checkNoChanges для того же набора представлений.

Подключение динамических View к ApplicationRef

Angular позволяет отрисовать компонент в отдельном DOM-элементе вне changeDetection Angular. Но поскольку эти View также нужно проверять, ApplicationRef реализует методы attachView() и detachView() для добавления/удаления в деревья обнаружения изменений. Это фактически добавляет эти представления в массив _views, который обходится во время цикла changeDetection.

Рассмотрим пример. У нас есть компонент M, который мы хотим создать динамически, а затем отрисовать в DOM вне основного дерева Angular:

@Component({
  selector: 'l-cmp',
  template: 'L'
})
export class L {
  constructor(moduleRef: NgModuleRef<any>, appRef: ApplicationRef) {
    const factory = moduleRef.componentFactoryResolver.resolveComponentFactory(M);
 
    let newNode = document.createElement('div');
    newNode.id = 'placeholder';
    document.body.prepend(newNode);
 
    const ref = factory.create(moduleRef.injector, [], newNode);
    appRef.attachView(ref.hostView);
  }
}
 
@Component({
  selector: 'm-cmp',
  template: '{{title}}'
})
export class M {
  title = 'I am the component that was created dynamically';
}

Вот что мы увидим, если проверим структуру DOM в приложении:

Если мы теперь посмотрим на свойство _views, то увидим следующее:

Мы можем использовать консоль, чтобы выяснить, что представляют собой экземпляры RootViewRef:

const TVIEW = 1;
const CONTEXT = 8;
const CHILD_HEAD = 13;
 
const view_1 = appRef._views[0];
const view_2 = appRef._views[1];
 
view_1._lView[TVIEW].type // 0 - HostView
view_1._lView[CONTEXT].constructor.name // M
 
view_1._lView[CHILD_HEAD][TVIEW].type // 0 - HostView
view_1._lView[CHILD_HEAD][CONTEXT].constructor.name // M
 
view_2._lView[CONTEXT].constructor.name // AppComponent (RootView)
view_2._lView[TVIEW].type // 0 - HostView
 
view_2._lView[CHILD_HEAD][CONTEXT].constructor.name // AppComponent (ComponentView)
view_2._lView[CHILD_HEAD][TVIEW].type // 1 - ComponentView
 
view_2._lView[CHILD_HEAD][CHILD_HEAD][CONTEXT].constructor.name // L

Диаграмма наглядно покажет эти взаимосвязи:

Несколько корневых компонентов одновременно

Angular даёт возможность установить несколько Root компонентов одновременно:

@NgModule({
  declarations: [ AppComponent, AppRootAnother ],
  imports: [  BrowserModule ],
  bootstrap: [ AppComponent, AppRootAnother ]
})
export class AppModule {}

при запуске приложения будет использовано несколько div в html тегах:

В таком случае при запуске приложения стоит не забыть их создать в index.html:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>LearnAngular</title>
</head>
<body>
  <app-root></app-root>
  <app-root-another></app-root-another>
</body>
</html>

С такой настройкой Angular создаст два независимых дерева changeDetection. Они будут зарегистрированы в ApplicationRef._views, и при вызове функции ApplicationRef.tick() Angular выполнит обнаружение изменений для обоих деревьев. По сути это аналогично использованию attachView,однако они все еще будут частью одного ApplicationRef, поэтому они будут использовать один инжектор, определенный для AppModule.

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