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
.
Это вольный перевод материала