简单地说,我在组件的模板中有一个元素。此元素具有ngIf
条件和(click)
处理程序。它不是从一开始就呈现的,因为 ngIf 条件的计算结果为false
。
现在有趣的部分来了:在角区域之外运行的代码将该条件更改为true
,并且在手动对更改检测器ref执行detectChanges
后,该元素被渲染并且点击处理程序ofc变为活动状态。
到目前为止,一切似乎都正常,但问题是,当用户单击时运行(click)
回调时,不会为组件触发更改检测。
这是复制 https://stackblitz.com/edit/angular-kea4wi
在那里重现它的步骤:
- 点击米色区域
- 按钮出现,也单击它
- 没有任何反应,尽管消息应该出现在下面
描述:
-
米色区域有一个通过 addEventListener 注册的单击事件处理程序,并且此事件侦听器的回调在角度区域之外运行。在其中,组件的
showButton
属性从false
设置为true
,我通过调用detectChanges()
手动触发更改检测,否则不会注册showButton
属性中的更改。代码如下所示:this.zone.runOutsideAngular(() => { const el = this.eventTarget.nativeElement as HTMLElement; el.addEventListener('click', e => { this.showButton = true; this.cd.detectChanges(); }) })
-
现在出现按钮,由于最初没有呈现
*ngIf="showButton"
,并且它具有在模板中声明的单击偶数处理程序。此处理程序再次更改组件的属性,这次showMessage
true
。<button *ngIf="showButton" (click)="onButtonClick()">Click me!</button> onButtonClick() { this.showMessage = true; }
-
当我单击它时,处理程序显然运行并将组件的
showMessage
更改为true
,但它不会触发更改检测,并且不会出现下面的消息。 要使示例正常工作,只需从一开始就将 showButton 设置为 true,上面的场景就可以了。
问题是:这怎么可能?由于我在模板中声明了(click)
事件处理程序,因此在调用时它不应该总是触发更改检测吗?
我在 Angular 的存储库中创建了一个问题,事实证明,这种行为是合乎逻辑的,尽管可能是出乎意料的。改写一下 Angular 团队在那里写的内容:
导致具有处理程序的元素呈现的代码(click)
如问题中所述在 Angular 区域之外运行。现在,虽然我在那里手动执行detectChanges()
,但这并不意味着代码突然神奇地在角度区域运行。它运行更改检测一切正常,但它">停留"在不同的区域。因此,当元素即将被渲染时,元素的单击回调将在非角度区域中创建并绑定到非角度区域。这反过来意味着,当用户单击触发它时,它仍会被调用,但不会触发更改检测。
解决方案是包装代码,该代码在角度区域之外运行,但需要在组件中执行一些更改,zone.run(() => {...})
.
因此,在我的堆栈闪电战复制中,在角区域之外运行的代码如下所示:
this.zone.runOutsideAngular(() => {
const el = this.eventTarget.nativeElement as HTMLElement;
el.addEventListener('click', e => {
this.zone.run(() => this.showButton = true);
})
})
这与调用detectChanges()
不同,这使this.showButton = true
在正确的区域中运行,因此由于使用其事件处理程序运行该代码而创建的元素也绑定到角度区域。这样,事件处理程序在响应 DOM 事件时始终触发更改检测。
这一切都归结为以下要点:在模板中声明事件处理程序并不能自动保证在所有方案中都检测到更改。
如果有人想要执行不触发更改检测的任务,方法如下:
import { NgZone }from '@angular/core';
taskSelection;
constructor
// paramenters
(
private _ngZone: NgZone,
)
// code block
{}
/*
Angular Lifecycle hooks
*/
ngOnInit() {
this.processOutsideOfAngularZone();
}
processOutsideOfAngularZone () {
var _this = this;
this._ngZone.runOutsideAngular(() => {
document.onselectionchange = function() {
console.log('Outside ngZone Done!');
let selection = document.getSelection();
_this.taskSelection["anchorNode"] = selection.anchorNode.parentElement;
_this.taskSelection["anchorOffset"] = selection.anchorOffset;
_this.taskSelection["focusOffset"] = selection.focusOffset;
_this.taskSelection["focusNode"] = selection.focusNode;
_this.taskSelection["rangeObj"] = selection.getRangeAt(0);
}
});
}
https://angular.io/api/core/NgZone#runOutsideAngular