#angular #angular-changedetection
Вопрос:
Давайте представим приложение, в котором
- все компоненты (включая приложение.компонент) являются
onPush
.
как я могу вызвать функцию forceAppWideChangeDetection()
, например app.component
, которая обеспечит обнаружение изменений в КАЖДОМ компоненте приложения.
Я представляю, что мне нужно пройти по внутреннему дереву компонентов и вызвать markForCheck для каждого компонента.
Как это можно сделать?
Пожалуйста, обратите внимание
- мое приложение все еще находится в ViewEngine. На случай, если это может иметь ко мне отношение.
- причина, по которой я спрашиваю об этом, заключается в следующем: когда язык в пользовательском интерфейсе меняется с lang-a на lang-b. Почти каждый компонент нуждается в определении изменений для обновления отображаемых текстов-языка. В настоящее время у нас есть собственный
@Input()
для этого, который проходит через все дерево компонентов и заставляет компоненты повторно отображаться как «свойство ввода изменилось». Лично мне такой подход не нравится, и я искал более простое решение. Так что, как вы можете видеть, этот дорогостоящий методforceAppWideChangeDetection()
должен выполняться только при изменении языка.
Комментарии:
1. Что происходит, когда вы звоните
this.changeDetectorRef.markForCheck();
вAppComponent
? AFAIK, он должен запустить обнаружение изменений для всего приложения. Вы уверены, что ваши изменения есть, когда вы отмечаете компонент для проверки?2. Он не будет запускать определение изменений для всех компонентов приложения, только для компонентов, включенных в
AppComponent
шаблон. Я следил за этим вопросом и создал песочницу stackblitz.com/edit/… вы можете проверить, какmarkForCheck
это работает, здесь. @s.алем3. markForCheck отметит только себя и всех родителей грязными.
4. @AndreElrico могу я спросить, почему вы хотите запустить проверку всего приложения? Не лучше ли было бы реагировать на изменения локально?
5. привет @s.alem Я обновил свой
please note
раздел и ответил на ваш вопрос там.
Ответ №1:
Если нет готового решения, о котором я не знаю, вы можете использовать службу и базовый класс:
@Injectable({ providedIn: 'root' })
export class ChangeDetectionTriggerService {
readonly trigger$ = new Subject<void>();
}
Затем базовые компоненты:
@Directive()
export class BaseComponent implements OnDestroy {
readonly onDestroy$ = new Subject<void>();
ngOnDestroy(): void {
this.onDestroy$.next();
}
}
@Directive() // https://angular.io/guide/migration-undecorated-classes
export class BaseChangeDetectionComponent implements OnInit extends BaseComponent {
constructor(private changeDetectorRef: ChangeDetectorRef,
private changeDetectionTriggerService: ChangeDetectionTriggerService) {
super();
}
ngOnInit(): void {
this.changeDetectionSub = changeDetectionTriggerService.trigger$
.pipe(takeUntil(this.onDestroy$))
.subscribe(() => this.changeDetectorRef.markForCheck());
}
}
Использование в целевом компоненте:
@Component(/** ... **/)
export class MyComponent extends BaseChangeDetectionComponent {
constructor(private changeDetectorRef: ChangeDetectorRef,
private changeDetectionTriggerService: ChangeDetectionTriggerService) {
super(this.changeDetectorRef, this.changeDetectionTriggerService);
}
}
Затем используйте его в любом месте, просто указав в теме:
changeDetectionTriggerService.trigger$.next();
Это, вероятно, сработало бы. Но оставь это на крайний случай. Надеюсь, есть более простое и элегантное решение.
Комментарии:
1. У меня была похожая идея, очень хорошее использование наследования. Проблема, с которой я столкнулся в этом случае, заключается в том, что я должен расширить каждый компонент. Тогда текущее решение кажется проще с помощью @Input и менее шаблонным.
2. Я не так уверен, потому что такой подход абстрагирует эту функциональность и позволяет вам писать меньше кода в ваших компонентах. Также вы можете включать, отключать, задерживать и т.д. В любом удобном для вас месте в центре. Еще одним преимуществом было бы добавление дополнительных функций в базовый класс, если вы захотите в будущем. Но я согласен, что это может быть излишним, если у вас есть только несколько компонентов.
Ответ №2:
В случае, если вы не боитесь использовать частный API, вы можете просмотреть все представления компонентов и пометить их как грязные
ViewEngine
import { ApplicationRef, Component } from '@angular/core';
@Component({
...
})
export class AnyComponent {
constructor(private appRef: ApplicationRef) {}
runCd() {
forceAppWideChangeDetection(this.appRef);
}
}
function markParentViewsForCheck(view) {
var currView = view;
while (currView) {
if (currView.def.flags amp; 2 /* OnPush */) {
currView.state |= 8 /* ChecksEnabled */;
}
currView = currView.viewContainerParent || currView.parent;
}
}
function forEachEmbeddedViews(view, visitorVn: (view) => void) {
const def = view.def;
if (!(def.nodeFlags amp; 16777216 /* EmbeddedViews */)) {
return;
}
for (var i = 0; i < def.nodes.length; i ) {
var nodeDef = def.nodes[i];
if (nodeDef.flags amp; 16777216 /* EmbeddedViews */) {
var embeddedViews = view.nodes[i].viewContainer._embeddedViews;
for (var k = 0; k < embeddedViews.length; k ) {
visitorVn(embeddedViews[k]);
}
} else if ((nodeDef.childFlags amp; 16777216) /* EmbeddedViews */ === 0) {
i = nodeDef.childCount;
}
}
}
function forEachComponentViews(view, visitorVn: (view) => void) {
const def = view.def;
if (!(def.nodeFlags amp; 33554432 /* ComponentView */)) {
return;
}
for (var i = 0; i < def.nodes.length; i ) {
var nodeDef = def.nodes[i];
if (nodeDef.flags amp; 33554432 /* ComponentView */) {
visitorVn(view.nodes[i].componentView);
}
else if ((nodeDef.childFlags amp; 33554432 /* ComponentView */) === 0) {
i = nodeDef.childCount;
}
}
}
function visitView(view) {
markParentViewsForCheck(view);
forEachEmbeddedViews(view, visitView);
forEachComponentViews(view, visitView);
}
function forceAppWideChangeDetection(appRef: ApplicationRef) {
for (const view of (appRef as any)._views) {
visitView(view._view);
}
}
Плющ
В Ivy вы можете перебирать все узлы вашего отрисованного компонента и использовать __ngContext__
их для пометки соответствующего представления как грязного.
import { Component, ɵmarkDirty } from '@angular/core';
@Component({
...
})
export class AnyComponent {
runCd() {
forceAppWideChangeDetection();
}
}
function forceAppWideChangeDetection() {
const CONTEXT = 8;
const PREFIX = 'app-'.toUpperCase();
const allHosts =
Array.from(document.querySelectorAll<any>('*'))
.filter(el => !!el.__ngContext__ amp;amp; el.tagName.startsWith(PREFIX));
for (const host of allHosts) {
const elementWithinHost = host.firstElementChild;
if (elementWithinHost amp;amp; elementWithinHost.__ngContext__) {
const component = elementWithinHost.__ngContext__[CONTEXT];
ɵmarkDirty(component)
}
}
}
Комментарии:
1. Ты просто потрясающая, спасибо. Я попробую эту версию Плюща завтра.
Ответ №3:
Вы можете загрузить это на каждый компонент, который хотите отслеживать, или просто на компонент приложения. это всего лишь свободный пример. Angular выполняет проверку жизненного цикла, которую можно вызвать с помощью крючка жизненного цикла onChanges — вы должны создать входные данные, такие как данные 0 и данные 1, для каждого родительского компонента.
Для этого создайте компонент, чтобы он мог прослушивать и импортировать его либо в AppComponent, либо в каждый родительский компонент компонента, который вы хотите отслеживать. Это может быть не совсем то, что вы хотите, но это будет очень близко.
из углового cli
список окон просмотра ng g c
import { Component, OnInit, OnChanges, SimpleChanges, Input } from '@angular/core';
@Component({
selector: 'app-viewport-listener',
template: '<div></div>',
styleUrls: ['./viewport-listener.component.css']
})
export class ViewportListenerComponent implements OnInit, OnChanges {
@Input() data: any;
constructor() { }
ngOnInit(): void {
}
ngOnChanges(changes: SimpleChanges){
forceAppWideDetction()
}
}
function forceAppWideDetction(){}
https://angular.io/api/core/OnChanges
https://www.stackchief.com/blog/ngOnChanges Example | Angular
https://dev.to/nickraphael/ngonchanges-best-practice-always-use-simplechanges-always-1feg
Комментарии:
1. привет, спасибо. Я бы использовал doCheck , если бы хотел добавить логику в каждый компонент. Я хотел бы решить эту проблему с корневого уровня, не добавляя никакой логики ни одному из детей.
2. @AndreElrico вы также можете использовать ссылку ChangeDetectorRef, которая сама прослушивает и помечает ее как грязную. angular.io/api/core/ChangeDetectorRef . У Angular есть много возможностей сделать то же самое. Однако создайте детектор в компоненте приложения.
3. да, но я хочу контролировать это с самого верхнего уровня. Корневой уровень ChangeDetectorRef мне не очень помогает.
4. Ммм, корневой уровень-это чувак верхнего уровня, это одно и то же. загрузите все, что вы тоже хотите прослушать, или напишите любые крючки для обнаружения изменений в компоненте приложения, который является родительским — другого компонента более высокого порядка нет, потому что он проходит через Dom — вы можете сделать это здесь, и он будет прослушивать изменения в ваших компонентах — но вам нужно установить базовую точку, и поскольку внесение изменений в компонент приложения не одобряется, загрузка компонента для прослушивания изменений в компоненте «верхнего уровня» или «корневого» — это путь.
Ответ №4:
Простой и чистый способ сделать это
Если вы хотите изменить язык всего приложения при смене языка без необходимости перезагрузки страницы, я бы рассмотрел следующее https://github.com/ngx-translate/core.
ngx-translate позволяет вам использовать канал для перевода языков, когда вы меняете язык внутри службы перевода ngx, тогда каждый канал загружает себя, и ваше приложение меняет свои языки без какой-либо другой суеты
<h2>{{ 'WELCOME' | translate }}</h2>
import { TranslateService } from '@ngx-translate/core'
//..,
constructor(private _translateService: TranslateService){}
setLanguage(language: string) {
this._translateService.use(language)
}
И вы закончили, я разрешаю вам прочитать это для получения более подробной информации.
Ответ на вопрос
Это мой собственный способ делать вещи, если бы у меня не было права использовать ngx-translate. Я бы создал «базовый» файл с необходимой вам логикой, мы поговорим здесь о наблюдаемом, я думаю
давайте назовем это base.component.ts
import { Directive, OnDestroy, OnInit } from '@angular/core'
import { Subject } from 'rxjs'
import { filter, takeUntil } from 'rxjs/operators'
import { LanguageService } from 'some/where'
@Directive()
export class BaseComponent implements OnInit, OnDestroy {
protected _unsubscribeAll: Subject<any> = new Subject()
constructor(public languageService: LanguageService) {}
ngOnInit(): void {
this.languageService.onLanguageChanged$
.pipe(
filter((langChange) => (langChange?.firstChange ? false : true)), // Avoid subscribing to the first change
takeUntil(this._unsubscribeAll),
)
.subscribe((value) => {
// Do the reload you need to
})
this._extendedInit()
}
protected _extendedInit() {
// For your default ngOnInit
}
ngOnDestroy(): void {
this._unsubscribeAll.next()
this._unsubscribeAll.complete()
this._extendedDestroy()
}
protected _extendedDestroy() {
// For your default ngOnDestroy
}
}
Затем вы расширяете каждый компонент, который необходимо обновить при изменении языка (или что-либо еще, если вам это нужно).
import { Directive } from '@angular/core'
import { LanguageService } from 'some/where'
import { BaseComponent } from 'some/where'
@Directive()
export class RandomComponent extends BaseComponent {
constructor(public languageService: LanguageService) {
super(languageService)
}
protected _extendedInit(): void {
// For custom ngOnInit
}
}
Если вам не нужно добавлять другой импорт в конструктор, вы можете проигнорировать его, как показано ниже
import { Directive } from '@angular/core'
@Directive()
export class RandomComponent extends BaseComponent {
protected _extendedInit(): void {
// For custom ngOnInit
}
}
И если нужно, вот как вы могли бы создать свой сервис.
Я добавил ту же логику, что и angular для ngOnChange, возможно, это поможет вам избежать перезагрузки, если вы этого не хотите, например, при первой загрузке
import { Injectable } from '@angular/core'
import { BehaviorSubject } from 'rxjs'
export interface LangChange {
previousValue: any
currentValue: any
firstChange: boolean
}
@Injectable({
providedIn: 'root',
})
export class LanguageService {
onSelectedLanguageChange$: BehaviorSubject<LangChange> = new BehaviorSubject(null)
constructor() {}
setLanguage(language: string) {
this.onSelectedLanguageChange$.next({
previousValue: this.onSelectedLanguageChange$.value,
currentValue: language,
firstChange: !this.onSelectedLanguageChange$.value,
})
}
}
затем, в ваш app.component
или куда угодно, куда вы хотите
constructor(private _languageService: LanguageService) {}
changeLang(language: string) {
this._languageService.setLanguage('de')
}