【问题标题】:Dynamically add event listener动态添加事件监听
【发布时间】:2016-05-06 22:39:42
【问题描述】:

我刚刚开始使用 Angular 2,我想知道是否有人可以告诉我从元素中动态添加和删除事件侦听器的最佳方法。

我设置了一个组件。单击模板中的某个元素时,我想将mousemove 的侦听器添加到同一模板的另一个元素中。然后我想在单击第三个元素时删除此侦听器。

我只是使用普通的 Javascript 来获取元素,然后调用标准的 addEventListener(),但我想知道是否有更“Angular2.0”的方式来做到这一点我应该调查一下。

【问题讨论】:

    标签: angular typescript addeventlistener


    【解决方案1】:

    我将添加 StackBlitz example 和对@tahiche 答案的评论。

    返回值是一个函数,用于在添加事件侦听器后将其移除。当您不再需要事件侦听器时,删除它们被认为是一种很好的做法。因此,您可以存储此返回值并在您的 ngOnDestroy 方法中调用它。

    我承认一开始它可能看起来令人困惑,但它实际上是一个非常有用的功能。你自己还能怎么清理?

    export class MyComponent implements OnInit, OnDestroy {
    
      public removeEventListener: () => void;
    
      constructor(
        private renderer: Renderer2, 
        private elementRef: ElementRef
      ) {
      }
    
      public ngOnInit() {
        this.removeEventListener = this.renderer.listen(this.elementRef.nativeElement, 'click', (event) => {
          if (event.target instanceof HTMLAnchorElement) {
            // Prevent opening anchors the default way
            event.preventDefault();
            // Your custom anchor click event handler
            this.handleAnchorClick(event);
          }
        });
      }
    
      public ngOnDestroy() {
        this.removeEventListener();
      }
    }
    

    您可以找到 a StackBlitz here 来展示这如何用于捕获对锚元素的点击。

    我添加了一个带有图像的正文,如下所示:
    <img src="x" onerror="alert(1)"></div>
    表明消毒剂正在发挥作用。

    Here in this fiddle 您会发现与innerHTML 相同的主体没有对其进行消毒,它将证明问题。

    【讨论】:

    • removeEventListener 有接口吗?据我了解,您也可以运行removeEventListener.unsubscribe() ?
    • @Mackelito unscubscribe 通常用于 Observables,侦听器没有 unsubscribe 方法。清理侦听器的方法是从EventManager (eventManager.remove(listener);) 中删除它们,或者如上所述调用返回的方法。
    【解决方案2】:

    我也觉得这非常令人困惑。 正如@EricMartinez 指出的那样,Renderer2 listen() 返回删除监听器的函数:

    ƒ () { return element.removeEventListener(eventName, /** @type {?} */ (handler), false); }
    

    如果我要添加一个监听器

    this.listenToClick = this.renderer.listen('document', 'click', (evt) => {
        alert('Clicking the document');
    })
    

    我希望我的函数能够执行我想要的,而不是完全相反的删除侦听器。

    // I´d expect an alert('Clicking the document'); 
    this.listenToClick();
    // what you actually get is removing the listener, so nothing...
    

    在给定的场景中,实际上将其命名为更有意义:

    // Add listeners
    let unlistenGlobal = this.renderer.listen('document', 'click', (evt) => {
        console.log('Clicking the document', evt);
    })
    
    let removeSimple = this.renderer.listen(this.myButton.nativeElement, 'click', (evt) => {
        console.log('Clicking the button', evt);
    });
    

    这一定是有充分理由的,但在我看来,这是非常具有误导性且不直观的。

    【讨论】:

    • 如果您要添加一个侦听器,为什么您会期望通过添加该侦听器返回的函数会调用该侦听器?这对我来说没有多大意义。添加侦听器的全部意义在于响应您不一定以编程方式触发的事件。我认为,如果您希望该函数调用您的侦听器,那么您可能还没有完全理解侦听器。
    • @tahiche mate 这真是令人困惑,感谢您指出这一点!
    • 它会返回这个,因此您也可以在稍后销毁组件时再次删除侦听器。添加侦听器时,最好在以后不再需要它们时将其删除。所以存储这个返回值并在你的ngOnDestroy 方法中调用它。我承认一开始它可能看起来令人困惑,但它实际上是一个非常有用的功能。自己还怎么清理?
    【解决方案3】:

    这是我的解决方法:

    我使用 Angular 6 创建了一个库。我添加了一个公共组件 commonlib-header,它在外部应用程序中像这样使用。

    注意serviceReference,它是包含stringFunctionName 方法的类(注入到使用commonlib-header 的组件constructor(public serviceReference: MyService) 中):

    <commonlib-header
        [logo]="{ src: 'assets/img/logo.svg', alt: 'Logo', href: '#' }"
        [buttons]="[{ index: 0, innerHtml: 'Button', class: 'btn btn-primary', onClick: [serviceReference, 'stringFunctionName', ['arg1','arg2','arg3']] }]">
        </common-header>
    

    库组件是这样编程的。在onClick(fn: any)方法中添加动态事件:

    export class HeaderComponent implements OnInit {
    
     _buttons: Array<NavItem> = []
    
     @Input()
      set buttons(buttons: Array<any>) {
        buttons.forEach(navItem => {
          let _navItem = new NavItem(navItem.href, navItem.innerHtml)
    
          _navItem.class = navItem.class
    
          _navItem.onClick = navItem.onClick // this is the array from the component @Input properties above
    
          this._buttons[navItem.index] = _navItem
        })
      }
    
      constructor() {}
    
      ngOnInit() {}
    
      onClick(fn: any){
        let ref = fn[0]
        let fnName = fn[1]
        let args = fn[2]
    
        ref[fnName].apply(ref, args)
      }
    

    可重复使用的header.component.html

    <div class="topbar-right">
      <button *ngFor="let btn of _buttons"
        class="{{ btn.class }}"
        (click)="onClick(btn.onClick)"
        [innerHTML]="btn.innerHtml | keepHtml"></button>
    </div>
    

    【讨论】:

      【解决方案4】:

      渲染器已在 Angular 4.0.0-rc.1 中被弃用,请阅读下面的更新

      angular2 方式是使用listenlistenGlobal from Renderer

      例如,如果您想向组件添加点击事件,则必须使用 Renderer 和 ElementRef(这也为您提供了使用 ViewChild 或任何检索 nativeElement 的选项)

      constructor(elementRef: ElementRef, renderer: Renderer) {
      
          // Listen to click events in the component
          renderer.listen(elementRef.nativeElement, 'click', (event) => {
            // Do something with 'event'
          })
      );
      

      您可以使用listenGlobal,这将使您可以访问documentbody等。

      renderer.listenGlobal('document', 'click', (event) => {
        // Do something with 'event'
      });
      

      请注意,从 beta.2 开始,listenlistenGlobal 都返回一个函数来删除侦听器(请参阅 beta.2 变更日志中的 breaking changes 部分)。这是为了避免大型应用程序中的内存泄漏(请参阅#6686)。

      所以要删除我们动态添加的监听器,我们必须将listenlistenGlobal 分配给一个变量,该变量将保存返回的函数,然后我们执行它。

      // listenFunc will hold the function returned by "renderer.listen"
      listenFunc: Function;
      
      // globalListenFunc will hold the function returned by "renderer.listenGlobal"
      globalListenFunc: Function;
      
      constructor(elementRef: ElementRef, renderer: Renderer) {
          
          // We cache the function "listen" returns
          this.listenFunc = renderer.listen(elementRef.nativeElement, 'click', (event) => {
              // Do something with 'event'
          });
      
          // We cache the function "listenGlobal" returns
          this.globalListenFunc = renderer.listenGlobal('document', 'click', (event) => {
              // Do something with 'event'
          });
      }
      
      ngOnDestroy() {
          // We execute both functions to remove the respectives listeners
      
          // Removes "listen" listener
          this.listenFunc();
          
          // Removs "listenGlobal" listener
          this.globalListenFunc();
      }
      

      这是一个plnkr,其中有一个示例。该示例包含listenlistenGlobal 的用法。

      在 Angular 4.0.0-rc.1+ 中使用 RendererV2 (从 4.0.0-rc.3 开始的 Renderer2)

      • 25/02/2017Renderer 已被弃用,现在我们应该使用 RendererV2(见下一行)。请参阅commit

      • 10/03/2017RendererV2 已重命名为 Renderer2。见breaking changes

      RendererV2 没有更多用于全局事件(文档、正文、窗口)的 listenGlobal 函数。它只有一个listen 函数,可以同时实现这两个功能。

      作为参考,我复制并粘贴了 DOM Renderer 实现的 source code,因为它可能会改变(是的,它是有角度的!)。

      listen(target: 'window'|'document'|'body'|any, event: string, callback: (event: any) => boolean):
            () => void {
          if (typeof target === 'string') {
            return <() => void>this.eventManager.addGlobalEventListener(
                target, event, decoratePreventDefault(callback));
          }
          return <() => void>this.eventManager.addEventListener(
                     target, event, decoratePreventDefault(callback)) as() => void;
        }
      

      如您所见,现在它会验证我们是否正在传递一个字符串(文档、正文或窗口),在这种情况下,它将使用内部 addGlobalEventListener 函数。在任何其他情况下,当我们传递一个元素(nativeElement)时,它将使用一个简单的addEventListener

      要删除监听器,它与 Angular 2.x 中的 Renderer 相同。 listen 返回一个函数,然后调用该函数。

      示例

      // Add listeners
      let global = this.renderer.listen('document', 'click', (evt) => {
        console.log('Clicking the document', evt);
      })
      
      let simple = this.renderer.listen(this.myButton.nativeElement, 'click', (evt) => {
        console.log('Clicking the button', evt);
      });
      
      // Remove listeners
      global();
      simple();
      

      plnkr 使用 Angular 4.0.0-rc.1 使用 RendererV2

      plnkr 使用 Angular 4.0.0-rc.3 使用 Renderer2

      【讨论】:

      • 这只是我使用 Angular2 的第二天,我才刚刚开始了解 v1,所以其中很多都是相当新的混乱。不过,您已经给了我很多要阅读的内容,因此我将关闭此内容,并且无疑会很快回来,并提出更多相关问题。为详细的回复干杯:)
      • @popClingwrap 您也可以查看HostListener。在文档中,检查 响应用户操作 下的 Attribute directives 以了解如何使用 host
      • @EricMartinez 有没有办法停止收听 listen 或 listenGlobal? (与 removeEventListener 相同)
      • @user1394625 是的,正如您在答案中看到的 ngOnDestroy 代码,listenlistenGlobal 返回一个函数,当调用/执行时,侦听器被删除。因此,如您所见,this.func 持有renderer.listen 返回的函数,而当我执行this.func() 时,我正在删除侦听器。 listenGlobal 也是如此。
      • @EricMartinez 又问了一个问题...如何访问函数内的“事件”来防止默认()或停止传播()
      猜你喜欢
      • 2012-10-10
      • 1970-01-01
      • 1970-01-01
      • 2021-08-15
      • 1970-01-01
      • 2020-06-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多