Выполнение обнаружения изменений Angular Ivy: вы готовы?

Выполнение обнаружения изменений Angular Ivy: вы готовы?

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

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

@Component({ selector: 'my-app', template: ` <h2>Parent</h2> <child [prop1]="x"></child> `
})
export class AppComponent { x = 1;
}
@Component({ selector: 'child', template: ` <h2>Child {{ prop1 }}</h2> <sub-child [item]="3"></sub-child> <sub-child *ngFor="let item of items" [item]="item"></sub-child> `
})
export class ChildComponent { @Input() prop1: number; items = [1, 2];
}
@Component({ selector: 'sub-child', template: ` <h2 (click)="clicked.emit()">Sub-Child {{ item }}</h2> <input (input)="text = $event.target.value"> <p>{{ text }}</p> `
})
export class SubChildComponent { @Input() item: number; @Output() clicked = new EventEmitter(); text: string;
}

Я создал онлайн-демоверсию, которую я использую, чтобы понять, как она работает: https://alexzuza.github.io/ivy-cd/

Демо использует компилятор angular 6.0.1 aot. Вы можете щелкнуть любой блок жизненного цикла, чтобы перейти к определению. Чтобы запустить процесс обнаружения изменений, просто введите что-то в одно из тех полей, которые ниже Sub-Child.

Представление

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

Root view | |___ AppComponent view | |__ ChildComponent view | |_ Embedded view | | | |_ SubChildComponent view | |_ Embedded view | | | |_ SubChildComponent view | |_ SubChildComponent view 

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

<h2>Child {{ prop1 }}</h2>
<sub-child [item]="3"></sub-child>
<sub-child *ngFor="let item of items" [item]="item"></sub-child>

В то время как текущий механизм представления создает узлы из фабрики определения представления и сохраняет их в массиве узлов определения представления

Ivy создает LNodes из инструкций, написанных в функции ngComponentDef.template, и сохраняет их в массиве данных:

Помимо узлов, новое представление также содержит привязки в массиве данных (см. data[4], data[5], data[6] на рисунке выше). Все привязки для данного представления сохраняются в том порядке, в котором они отображаются в шаблоне, начиная с bindingStartIndex

Обратите внимание, как я получаю экземпляр просмотра из ChildComponent. ComponentInstance .__ ngHostLNode__ содержит ссылку на узел узла компонента. (Другой способ — ввести ChangeDetectorRef)

Таким образом, Angular сначала создает корневой вид и находит хост-элемент в индексе 0 в массиве data

RootView data: [LNode] native: root component selector

а затем проходит через все компоненты и заполняет массив data для каждого представления.

Обнаружение изменений

Хорошо известный ChangeDetectorRef — это просто абстрактный класс с абстрактными методами, такими как detectChanges, markForCheck и т. д.

Когда мы запрашиваем эту зависимость в конструкторе компонента, мы фактически получаем экземпляр ViewRef, который расширяет класс ChangeDetectorRef.

Теперь давайте рассмотрим внутренние методы, которые используются для запуска обнаружения изменений в Ivy. Некоторые из них доступны как public api (markViewDirty и detectChanges ), но я не уверен в других.

detectChanges

Синхронно выполняет обнаружение изменений на компоненте (и, возможно, его подкомпонентах).

Эта функция запускает обнаружение изменений синхронно на компоненте. Существует очень мало причин для вызова этой функции напрямую, так как предпочтительным способом обнаружения изменений является использование markDirty (см. ниже) и дождаться, когда планировщик вызовет этот метод в какой-то будущий момент. Это связано с тем, что одно действие пользователя часто приводит к тому, что многие компоненты становятся недействительными, а одновременное обнаружение изменений на каждом компоненте будет неэффективным. Лучше подождать, пока все компоненты не будут отмечены как загрязненные, а затем выполните единое обнаружение изменений по всем компонентам

export function detectChanges<T>(component: T): void { const hostNode = _getComponentHostLElementNode(component); ngDevMode && assertNotNull(hostNode.data, 'Component host node should be attached to an LView'); const componentIndex = hostNode.tNode !.flags >> TNodeFlags.DirectiveStartingIndexShift; const def = hostNode.view.tView.directives ![componentIndex] as ComponentDef<T>; detectChangesInternal(hostNode.data as LView, hostNode, def, component);
}

tick

Используется для обнаружения изменений во всем приложении. Это эквивалентно `detectChanges`, но вызывается на корневой компонент. Кроме того, `tick` выполняет хуки жизненного цикла и условно проверяет компоненты на основе их` ChangeDetectionStrategy` и грязи.

export function tick<T>(component: T): void { const rootView = getRootView(component); const rootComponent = (rootView.context as RootContext).component; const hostNode = _getComponentHostLElementNode(rootComponent); ngDevMode && assertNotNull(hostNode.data, 'Component host node should be attached to an LView'); renderComponentOrTemplate(hostNode, rootView, rootComponent);
}

scheduleTick

Используется для планирования обнаружения изменений для всего приложения. В отличие от tick, scheduleTick объединяет несколько вызовов в один проход обнаружения изменений. Обычно это называется косвенно, вызывая markDirty когда представление нужно повторно markDirty.

export function scheduleTick<T>(rootContext: RootContext) { if (rootContext.clean == _CLEAN_PROMISE) { let res: null|((val: null) => void); rootContext.clean = new Promise<null>((r) => res = r); rootContext.scheduler(() => { tick(rootContext.component); res !(null); rootContext.clean = _CLEAN_PROMISE; }); }
}

markViewDirty (markForCheck)

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

export function markViewDirty(view: LView): void { let currentView: LView|null = view; while (currentView.parent != null) { currentView.flags |= LViewFlags.Dirty; currentView = currentView.parent; } currentView.flags |= LViewFlags.Dirty; ngDevMode && assertNotNull(currentView !.context, 'rootContext'); scheduleTick(currentView !.context as RootContext);
}

markDirty

Помечает компонент как грязный (требуется обнаружение изменений). Отметка о загрязнении компонента запланирует обнаружение изменений на этом компоненте в какой-то момент в будущем. Пометка уже грязного компонента как грязного — это заглушка. Только одно замечательное обнаружение изменений может быть запланировано для каждого дерева компонентов. (Два компонента, загруженные с помощью отдельного «renderComponent», будут иметь отдельные планировщики)

export function markDirty<T>(component: T) { ngDevMode && assertNotNull(component, 'component'); const lElementNode = _getComponentHostLElementNode(component); markViewDirty(lElementNode.view);
}

checkNoChanges

Ничего нового:) Когда я отлаживал новый механизм обнаружения изменений, я заметил, что забыл intall zone.js. И, как вы уже догадались, он отлично работал без этой зависимости и без cdRef.detectChanges или tick.

Почему?

Как вы, вероятно, знаете, по дизайну триггеры Angular меняют обнаружение для компонента onPush только в том случае, если (см. мой ответ на stackoverflow).

Эти правила также применяются к Ivy:

Меняется одно из полей https://github.com/angular/angular/blob/43d62029f0e2da0150ba6f09fd8989ca6391a355/packages/core/src/render3/instructions.ts#L890

Связанное событие, вызванное компонентом или его дочерними элементами https://github.com/angular/angular/blob/43d62029f0e2da0150ba6f09fd8989ca6391a355/packages/core/src/render3/instructions.ts#L1743

Ручной вызов markForCheck (функция markViewDirty отвечает за это сейчас (см. ниже))

У меня есть (input) выходное связывание в SubChildComponent. Второе правило приведет к вызову markForCheck. Поскольку мы уже узнали, что этот метод фактически вызывает обнаружение изменений, теперь должно быть ясно, как это работает без zonejs.

А если выражение изменилось после проверки?

Не волнуйся, он все еще здесь :)

Порядок изменения обнаружения

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

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

Вернемся к ChildComponent в моем простом приложении

<h2>Child {{ prop1 }}</h2>
<sub-child [item]="3"></sub-child>
<sub-child *ngFor="let item of items" [item]="item"></sub-child>

С моей стороны было предназначено написать один sub-child как обычный компонент перед другими, которые находятся внутри встроенного представления. Теперь пришло время увидеть это в действии:

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

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

https://alexzuza.github.io/ivy-cd/

Инициализация однократной строки

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

<comp color="#efefef"></comp> 

Это так называемая однократная инициализация строк. В документации Angular сказано:

Angular устанавливает его и забывает об этом.

Что касается меня, это означает, что Angular не будет делать никаких дополнительных проверок для этой привязки. Но то, что мы фактически видим в angular5 — это то, что он проверяется за каждый цикл обнаружения изменений во время вызова updateDirectives.

function updateDirectives(_ck,_v) { var currVal_0 = '#efefef';
_ck(_v,1,0,currVal_0); 

Теперь давайте посмотрим, как это должно быть в новом движке:

var _c0 = ["color", "#efefef"];
AppComponent.ngComponentDef = i0.ɵdefineComponent({ type: AppComponent, selectors: [["my-app"]], ... template: function AppComponent_Template(rf, ctx) { // create mode if (rf & 1) { i0.ɵE(0, "child", _c0); <========== used only in create mode i0.ɵe(); } if (rf & 2) { ... } }
}) 

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

Обновление: https://github.com/angular/angular/pull/24346

Даже если вы не знаете, как работает Angular ViewContainer под капотом, вы можете заметить следующую картину при открытии devtools:

В режиме production мы видим только <!——>. И вот вывод Ivy:

Я не могу быть уверен в 100%, но, похоже, у нас будет такой результат, как только Ivy станет стабильным. В результате query в коде ниже

@Component({ ..., template: '<ng-template #foo></ng-template>'
})
class SomeComponent { @ViewChild('foo', {read: ElementRef}) query;
} 

будет возвращать null, поскольку Angular не должен больше читать ElementRef с помощью собственного элемента, указывающего на комментарий узла DOM из контейнеров

Инкрементный DOM (IDOM) с нуля

Давным-давно Google анонсировала так называемую инкрементальную библиотеку DOM.

Библиотека фокусируется на создании DOM-деревьев и предоставлении динамических обновлений. Она не предназначался для использования напрямую, а как цель компиляции для шаблонных движков. И, похоже, у IVy есть что-то общее с инкрементной библиотекой DOM.

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

Предположим, что на странице уже есть элемент input и button:

<input type="text" value="Alexey">
<button>Increment</button> 

И все, что нам нужно сделать, это визуализировать динамический html, который будет выглядеть так:

<h1>Hello, Alexey</h1>
<ul> <li> Counter: <span>1</span> </li>
</ul> 

Чтобы сделать это, давайте напишем elementOpen, elementClose и текстовые «инструкции» (я называю это так, потому что Angular использует такие имена, как IVy, можно рассматривать как особый вид виртуального процессора).

Сначала нам нужно написать специальные помощники для перемещения дерева узлов:

// The current nodes being processed
let currentNode = null;
let currentParent = null; function enterNode() { currentParent = currentNode; currentNode = null;
}
function nextNode() { currentNode = currentNode ? currentNode.nextSibling : currentParent.firstChild;
}
function exitNode() { currentNode = currentParent; currentParent = currentParent.parentNode;
} 

Теперь давайте напишем инструкции:

function renderDOM(name) { const node = name === '#text' ? document.createTextNode('') : document.createElement(name); currentParent.insertBefore(node, currentNode); currentNode = node; return node;
} function elementOpen(name) { nextNode(); const node = renderDOM(name); enterNode(); return currentParent;
} function elementClose(node) { exitNode(); return currentNode;
} function text(value) { nextNode(); const node = renderDOM('#text'); node.data = value; return currentNode;
} 

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

Мы хотим, чтобы наши элементы были способны поддерживать некоторое состояние, поэтому давайте представим NodeData:

const NODE_DATA_KEY = '__ID_Data__'; class NodeData { // key // attrs constructor(name) { this.name = name; this.text = null; }
} function getData(node) { if (!node[NODE_DATA_KEY]) { node[NODE_DATA_KEY] = new NodeData(node.nodeName.toLowerCase()); } return node[NODE_DATA_KEY];
} 

Теперь давайте изменим нашу функцию renderDOM чтобы мы не добавили новый элемент в DOM, если в текущей позиции уже есть то же самое:

const matches = function(matchNode, name/*, key */) { const data = getData(matchNode); return name === data.name // && key === data.key;
}; function renderDOM(name) { if (currentNode && matches(currentNode, name/*, key */)) { return currentNode; } ...
} 

Обратите внимание на мой комментарий /*, key */. Было бы лучше, если бы у наших элементов был некоторый ключ к различию элементов. После этого добавим логику, которая будет отвечать за обновления текстовых узлов

function text(value) { nextNode(); const node = renderDOM('#text'); // update // checks for text updates const data = getData(node); if (data.text !== value) { data.text = (value); node.data = value; } // end update return currentNode;
} 

То же самое можно сделать для узлов элементов.

Затем давайте напишем функцию patch, которая будет принимать элемент DOM, функцию обновления и некоторые данные, которые будут потребляться функцией обновления:

function patch(node, fn, data) { currentNode = node; enterNode(); fn(data); exitNode();
}; 

Наконец, давайте протестируем наши инструкции:

function render(data) { elementOpen('h1'); { text('Hello, ' + data.user) } elementClose('h1'); elementOpen('ul') { elementOpen('li'); { text('Counter: ') elementOpen('span'); { text(data.counter); } elementClose('span'); } elementClose('li'); } elementClose('ul');
} document.querySelector('button').addEventListener('click', () => { data.counter ++; patch(document.body, render, data);
});
document.querySelector('input').addEventListener('input', (e) => { data.user = e.target.value; patch(document.body, render, data);
}); const data = { user: 'Alexey', counter: 1
}; patch(document.body, render, data);

Результат можно найти здесь

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

Таким образом, основная концепция IDOM заключается в том, чтобы просто использовать реальный DOM для сравнения с новыми деревьями. Это все. Спасибо за прочтение…

Автор: Alexey Zuev

Источник: https://blog.angularindepth.com/

Редакция: Команда webformyself.