角度单击事件处理程序未触发更改检测

Posted

技术标签:

【中文标题】角度单击事件处理程序未触发更改检测【英文标题】:Angular click event handler not triggering change detection 【发布时间】:2020-08-26 00:00:19 【问题描述】:

简单地说,我在组件的模板中有一个元素。这个元素有一个ngIf 条件和一个(click) 处理程序。它不是从一开始就呈现的,因为 ngIf 条件的计算结果为 false

现在到了有趣的部分:在角度区域外部运行的代码将该条件更改为true,并且在手动对更改检测器引用执行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) 事件处理程序,它不应该总是在调用时触发更改检测吗?

【问题讨论】:

带有 stackblitz 演示的重现步骤会很棒! 我的猜测是单击时会触发更改检测,但是由于它从根组件开始并且根组件具有OnPush,因此它会停在那里并且不会检查组件树下的其他组件。 @alt255 点击事件应该绕过OnPush 层次结构,并在适当的位置触发更改检测 - 在它冒泡的组件中(DOM 方式)。否则这将是一个相当普遍的问题和 Angular 的设计缺陷。 see this for details 这验证了我上面的观点 @alt255 如果是这种情况,为什么在描述的场景之外单击箭头会起作用呢?就像我用画廊刷新页面并多次单击下一步和上一个箭头一样。它会更新计数器。 【参考方案1】:

我在 Angular 的 repo 中创建了an issue,事实证明,这种行为是合乎逻辑的,尽管可能出乎意料。重新表述 Angular 团队在那里写的内容:

导致带有(click) 处理程序的元素呈现的代码在问题中所述的 Angular 区域之外运行。现在,虽然我在那里手动执行detectChanges(),但这并不意味着代码突然神奇地在角度区域运行。它可以正常运行更改检测,但它“停留”在不同的区域。结果,当元素即将被渲染时,元素的点击回调被创建并绑定到非角度区域。这反过来意味着当它被用户点击触发时,它仍然被调用,但不会触发更改检测

解决方案是在zone.run(() =&gt; ...)中包装代码,该代码在角度区域之外运行,但需要在组件中执行一些更改。

所以在我的 stackblitz 复制中,在角度区域之外运行的代码如下所示:

    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 事件时总是触发更改检测

这一切都归结为以下要点:在模板中声明事件处理程序 并不能自动保证在所有情况下都能检测到更改

【讨论】:

在任何地方都做ngZone不会影响性能或应用程序,如果它有点大? @PhanendraCharyuluKanduri 使用 runOutsideAngular 可以非常积极地影响您的应用程序的性能,无论大小。特别是如果用于处理 Angular 不必“知道”的事件,否则会导致它为每个事件运行更改检测。

以上是关于角度单击事件处理程序未触发更改检测的主要内容,如果未能解决你的问题,请参考以下文章

类事件处理程序未检测是不是选中复选框

角度 2(更改)事件未触发

用于ComboBox项目选择的事件处理程序(选定项目未必更改)

模糊事件未在角度分量上触发

为啥单击事件处理程序会在页面加载后立即触发?

带有 mousedown 事件处理程序的 AngularJS 指令,但单击事件永远不会触发