将整个应用程序标记为脏并强制更改检测
mark entire app dirty and force changedetection
让我们想象一个应用,其中
- 所有组件(包括 app.component)都是
onPush
。
如何在 app.component
中调用函数 forceAppWideChangeDetection()
以确保 运行 在应用程序的每个组件中进行 changeDetection。
我想我必须遍历组件的内部树并在每个组件上调用 markForCheck。
如何做到这一点?
请注意
我的应用仍在 viewEngine 上。如果这可能与我相关。
我问这个的原因是:当 UI 中的语言从 lang-a 更改为 lang-b 时。几乎每个组件都需要一个 changeDetection 来更新显示的文本语言。目前我们有一个自己的 @Input()
,它遍历整个组件树并强制组件重新呈现为“输入 属性 已更改”。我个人不喜欢这种方法,正在寻找更简单的解决方案。因此,正如您所见,这种昂贵的方法 forceAppWideChangeDetection()
仅在语言更改时 运行。
您可以在每个要监控的组件或仅应用组件上加载它。这只是一个松散的例子。 Angular 执行 生命周期检查 可以使用 OnChanges 生命周期钩子调用 -您应该在每个父组件上创建一个输入,例如数据 0 和数据 1。
要做到这一点 创建 一个 组件 来做到这一点以便它可以收听和 导入 它进入 或者 appComponent 或者 进入每个组件 parent 你要监控的组件。这可能不是您想要的,但非常接近。
来自 angular cli
ng g c viewportListener
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%20Example%20%7C%20Angular
https://dev.to/nickraphael/ngonchanges-best-practice-always-use-simplechanges-always-1feg
除非有我不知道的现成解决方案,否则您可以使用服务和基础 class:
@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();
这可能会奏效。但保留它作为最后的手段。希望有更简单优雅的解决方案。
简单明了的方法
如果您希望在更改语言时更改整个应用程序的语言而无需重新加载页面,我会考虑查看以下内容 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)
}
大功告成,我让你阅读 this 了解更多详情。
回答问题
如果我没有使用 ngx-translate 的权利,这是我自己的做事方式。我会创建一个“基础”文件,逻辑你需要,我认为我们将在这里讨论 observable
我们给它起个名字吧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,可以忽略,如下
import { Directive } from '@angular/core'
@Directive()
export class RandomComponent extends BaseComponent {
protected _extendedInit(): void {
// For custom ngOnInit
}
}
如果需要,这就是您创建服务的方式。
我确实为 ngOnChange 添加了与 angular 相同的逻辑,如果您不想这样做,也许可以帮助您避免重新加载东西,例如第一次加载
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')
}
如果你不怕使用 private 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 & 2 /* OnPush */) {
currView.state |= 8 /* ChecksEnabled */;
}
currView = currView.viewContainerParent || currView.parent;
}
}
function forEachEmbeddedViews(view, visitorVn: (view) => void) {
const def = view.def;
if (!(def.nodeFlags & 16777216 /* EmbeddedViews */)) {
return;
}
for (var i = 0; i < def.nodes.length; i++) {
var nodeDef = def.nodes[i];
if (nodeDef.flags & 16777216 /* EmbeddedViews */) {
var embeddedViews = view.nodes[i].viewContainer._embeddedViews;
for (var k = 0; k < embeddedViews.length; k++) {
visitorVn(embeddedViews[k]);
}
} else if ((nodeDef.childFlags & 16777216) /* EmbeddedViews */ === 0) {
i += nodeDef.childCount;
}
}
}
function forEachComponentViews(view, visitorVn: (view) => void) {
const def = view.def;
if (!(def.nodeFlags & 33554432 /* ComponentView */)) {
return;
}
for (var i = 0; i < def.nodes.length; i++) {
var nodeDef = def.nodes[i];
if (nodeDef.flags & 33554432 /* ComponentView */) {
visitorVn(view.nodes[i].componentView);
}
else if ((nodeDef.childFlags & 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__ && el.tagName.startsWith(PREFIX));
for (const host of allHosts) {
const elementWithinHost = host.firstElementChild;
if (elementWithinHost && elementWithinHost.__ngContext__) {
const component = elementWithinHost.__ngContext__[CONTEXT];
ɵmarkDirty(component)
}
}
}
让我们想象一个应用,其中
- 所有组件(包括 app.component)都是
onPush
。
如何在 app.component
中调用函数 forceAppWideChangeDetection()
以确保 运行 在应用程序的每个组件中进行 changeDetection。
我想我必须遍历组件的内部树并在每个组件上调用 markForCheck。
如何做到这一点?
请注意
我的应用仍在 viewEngine 上。如果这可能与我相关。
我问这个的原因是:当 UI 中的语言从 lang-a 更改为 lang-b 时。几乎每个组件都需要一个 changeDetection 来更新显示的文本语言。目前我们有一个自己的
@Input()
,它遍历整个组件树并强制组件重新呈现为“输入 属性 已更改”。我个人不喜欢这种方法,正在寻找更简单的解决方案。因此,正如您所见,这种昂贵的方法forceAppWideChangeDetection()
仅在语言更改时 运行。
您可以在每个要监控的组件或仅应用组件上加载它。这只是一个松散的例子。 Angular 执行 生命周期检查 可以使用 OnChanges 生命周期钩子调用 -您应该在每个父组件上创建一个输入,例如数据 0 和数据 1。
要做到这一点 创建 一个 组件 来做到这一点以便它可以收听和 导入 它进入 或者 appComponent 或者 进入每个组件 parent 你要监控的组件。这可能不是您想要的,但非常接近。
来自 angular cli
ng g c viewportListener
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%20Example%20%7C%20Angular https://dev.to/nickraphael/ngonchanges-best-practice-always-use-simplechanges-always-1feg
除非有我不知道的现成解决方案,否则您可以使用服务和基础 class:
@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();
这可能会奏效。但保留它作为最后的手段。希望有更简单优雅的解决方案。
简单明了的方法
如果您希望在更改语言时更改整个应用程序的语言而无需重新加载页面,我会考虑查看以下内容 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)
}
大功告成,我让你阅读 this 了解更多详情。
回答问题
如果我没有使用 ngx-translate 的权利,这是我自己的做事方式。我会创建一个“基础”文件,逻辑你需要,我认为我们将在这里讨论 observable
我们给它起个名字吧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,可以忽略,如下
import { Directive } from '@angular/core'
@Directive()
export class RandomComponent extends BaseComponent {
protected _extendedInit(): void {
// For custom ngOnInit
}
}
如果需要,这就是您创建服务的方式。
我确实为 ngOnChange 添加了与 angular 相同的逻辑,如果您不想这样做,也许可以帮助您避免重新加载东西,例如第一次加载
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')
}
如果你不怕使用 private 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 & 2 /* OnPush */) {
currView.state |= 8 /* ChecksEnabled */;
}
currView = currView.viewContainerParent || currView.parent;
}
}
function forEachEmbeddedViews(view, visitorVn: (view) => void) {
const def = view.def;
if (!(def.nodeFlags & 16777216 /* EmbeddedViews */)) {
return;
}
for (var i = 0; i < def.nodes.length; i++) {
var nodeDef = def.nodes[i];
if (nodeDef.flags & 16777216 /* EmbeddedViews */) {
var embeddedViews = view.nodes[i].viewContainer._embeddedViews;
for (var k = 0; k < embeddedViews.length; k++) {
visitorVn(embeddedViews[k]);
}
} else if ((nodeDef.childFlags & 16777216) /* EmbeddedViews */ === 0) {
i += nodeDef.childCount;
}
}
}
function forEachComponentViews(view, visitorVn: (view) => void) {
const def = view.def;
if (!(def.nodeFlags & 33554432 /* ComponentView */)) {
return;
}
for (var i = 0; i < def.nodes.length; i++) {
var nodeDef = def.nodes[i];
if (nodeDef.flags & 33554432 /* ComponentView */) {
visitorVn(view.nodes[i].componentView);
}
else if ((nodeDef.childFlags & 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__ && el.tagName.startsWith(PREFIX));
for (const host of allHosts) {
const elementWithinHost = host.firstElementChild;
if (elementWithinHost && elementWithinHost.__ngContext__) {
const component = elementWithinHost.__ngContext__[CONTEXT];
ɵmarkDirty(component)
}
}
}