Angular事件管理插件(EVENT_MANAGER_PLUGINS)

1,501 阅读4分钟

简述

Angular是一款非常流行的前端框架,它具有强大的事件管理功能。Angular事件管理插件(EVENT_MANAGER_PLUGINS)是Angular中的一个重要组成部分,可以帮助我们更好地管理应用程序中的事件。

在本文中,我将详细介绍Angular事件管理插件的作用和原理,并提供一些使用Angular事件管理插件的最佳实践。

Angular中注册事件的几种常见方式

  • 使用template语法来注册事件监听 (eventName)="eventHandler($event)"
    事件绑定是Angular中最常用的事件监听方式之一。我们可以使用事件绑定语法,将组件中的属性绑定到DOM元素的事件上。当事件触发时,Angular会调用相应的组件方法。 例如:
    <button (click)="onClick()">Click me</button>
    
  • 使用@HostListener装饰器来监听宿主元素上的事件
    HostListener装饰器是Angular提供的另一种事件监听方式(监听对象是宿主)。它可以让我们在组件类中直接声明事件监听器,而无需在模板中使用属性绑定语法。
    例如 :
    import { Component, HostListener } from '@angular/core';
    
    @Component({
         selector: 'app-example',
         template: '<button>Click me</button>'
    })
    export class ExampleComponent {
         @HostListener('click')
         onClick() {
           // 执行一些操作
         }
    }
    
    组件类中使用@HostListener装饰器声明了一个名为onClick的方法,它会在按钮被点击时被调用。
  • 使用内置的Renderer2来监听相应的事件
    Renderer2是Angular提供的另一个事件监听方式。它允许我们直接操作DOM元素,以及在DOM元素上添加事件监听器。
    例如,以下代码使用Renderer2在按钮上添加了click事件监听器:
    import { Component, ElementRef, Renderer2 } from '@angular/core';
    
    @Component({
         selector: 'app-example',
         template: '<button>Click me</button>'
    })
    export class ExampleComponent {
         constructor(private elementRef: ElementRef, private renderer: Renderer2) {}
    
         ngAfterViewInit() {
           const button = this.elementRef.nativeElement.querySelector('button');
           this.renderer.listen(button, 'click', () => {
             // 执行一些操作
             return false // 返回false可以取消浏览器的事件默认行为 后面有源码印证
           });
         }
    }
    
    
    这里使用Renderer2的listen方法在按钮上添加了click事件监听器。注意,我们需要使用ElementRef获取对DOM元素的引用, 为啥返回false可以取消浏览器的事件默认行为可以参考

Angular事件管理插件的作用

在Angular中,EVENT_MANAGER_PLUGINS是事件管理器的核心组件之一。它是Angular事件系统的底层实现,用于管理所有DOM事件。当我们在Angular组件中使用@HostListenerRenderer2服务等API来监听DOM事件时,实际上是通过EVENT_MANAGER_PLUGINS来实现的。

EVENT_MANAGER_PLUGINS本身并不会处理任何事件,它仅仅是一个容器,用于存储一系列事件插件。每个事件插件都有自己的事件类型、目标元素和事件处理函数等,当事件触发时,EVENT_MANAGER_PLUGINS会将事件传递给所有已注册的事件插件,让它们依次处理事件。

EVENT_MANAGER_PLUGINS插件在Angular中有着广泛的应用,它们可以用来实现各种各样的功能,例如:

  • 处理自定义事件:我们可以编写自定义事件插件来处理非标准的DOM事件,例如RxJS的Observable。
  • 实现runOutsideAngular:我们可以编写事件插件,将某些事件处理函数放到Angular变更检测之外执行,因此不会触发Angular的变更检测,从而避免了额外的性能开销。
  • 扩展事件处理:我们可以编写事件插件,对事件处理函数进行拦截和修改,以实现各种特殊的需求, 比如在模板语法中拓展回车事件

总之上述的几种注册方式,底层依赖了事件管理
     当angular默认事件管理插件无法满足需求时,允许我们自定义事件管理器,而我们在注册事件方式上依旧没有变,还是上述的几种注册方式,所以通过自定义事件管理器,我们可以实现更灵活、更优雅、更高效的事件管理。

Angular事件管理插件的原理

以使用内置的Renderer2来监听相应的事件为例, 从源码的角度来看事件管理插件的原理
Renderer2 实现类部分源码如下:

function decoratePreventDefault(eventHandler: Function): Function {
  return (event: any) => {
    // ... 省略部分代码
    const allowDefaultBehavior = eventHandler(event);
    // 这就是为啥我们在eventHandler中返回false可以取消浏览器的默认行为
    if (allowDefaultBehavior === false) {
      event.preventDefault();
      event.returnValue = false;
    }

    return undefined;
  };
}

class DefaultDomRenderer2 implements Renderer2 {
  constructor(private eventManager: EventManager) {}

  // ... 省略一些无关的属性和方法

  listen(target: 'window'|'document'|'body'|any, event: string, callback: (event: any) => boolean):
      () => void {
    // 开发环境 检查以@开头的事件名称 -- 不允许以@开头的事件名称
    NG_DEV_MODE && checkNoSyntheticProp(event, 'listener');
    if (typeof target === 'string') {
      // target 为 "window", "document", or "body"
      return <() => void>this.eventManager.addGlobalEventListener(
          target, event, decoratePreventDefault(callback));
    }
    // EventManager事件管理器 内部管理事件管理插件
    return <() => void>this.eventManager.addEventListener(
               target, event, decoratePreventDefault(callback)) as () => void;
  }
}

上述代码很少,主要是通过事件管理器来注册事件监听,decoratePreventDefault函数用于处理浏览器事件默认行为的
EventManager 部分源码如下:

@Injectable()
export class EventManager {
  // 存储事件插件的数组
  private _plugins: EventManagerPlugin[];
  private _eventNameToPlugin = new Map<string, EventManagerPlugin>();

  constructor(@Inject(EVENT_MANAGER_PLUGINS) plugins: EventManagerPlugin[], private _zone: NgZone) {
    plugins.forEach((plugin) => {
      plugin.manager = this;
    });
    // 插件顺序反转 也就是说后注册的优先级更高 这就是为什么可以覆盖Angular的默认插件
    this._plugins = plugins.slice().reverse();
  }

 /**
    * @param element 接收事件通知的 HTML 元素。
    * @param eventName 要监听的事件名称。
    * @param handler 通知发生时调用的函数。
    * @returns 可用于删除处理程序的回调函数。可以使用回调来实现释放资源
    */
  addEventListener(element: HTMLElement, eventName: string, handler: Function): Function {
    const plugin = this._findPluginFor(eventName);
    return plugin.addEventListener(element, eventName, handler);
  }

  /**
   * 获取zone 这个在后面我们自定义事件管理插件会使用到
   */
  getZone(): NgZone {
    return this._zone;
  }

  _findPluginFor(eventName: string): EventManagerPlugin {
    const plugin = this._eventNameToPlugin.get(eventName);
    if (plugin) {
      return plugin;
    }

    const plugins = this._plugins;
    for (let i = 0; i < plugins.length; i++) {
      const plugin = plugins[i];
      if (plugin.supports(eventName)) {
        // 缓存查询结果 下次遇到相同的事件名称直接放回结果,提供性能
        this._eventNameToPlugin.set(eventName, plugin);
        return plugin;
      }
    }
    throw new Error(`No event manager plugin found for event ${eventName}`);
  }
}

上述代码通过依赖注入获取注册的事件管理插件,然后反转插件顺序来实现后注册的插件优先,也就是说后注册的优先级更高,这样就可以实现覆盖Angular的默认插件

自定义事件管理插件

如果默认事件管理插件不能满足我们的需求,我们可以编写自定义事件管理插件来处理自定义事件。其实我们从源码就可以大概知道如何去注册,在源码中通过EVENT_MANAGER_PLUGINS令牌来获取事件管理插件,那么我们只需要注册这个提供商即可,创建自定义事件管理插件需要遵循以下步骤:

  1. 实现EventManagerPlugin接口。
  2. 在模块中providers数组中添加自定义事件管理插件。

下面我们将详细讨论每个步骤。

  1. 实现EventManagerPlugin接口

自定义事件管理插件必须实现EventManagerPlugin接口,该接口定义了一组方法来处理事件。以下是EventManagerPlugin接口的定义:

export interface EventManagerPlugin {
    supports(eventName: string): boolean;
    addEventListener(element: HTMLElement, eventName: string, handler: Function): Function;
    addGlobalEventListener(element: string, eventName: string, handler: Function): Function;
  }
  • supports(eventName: string):该方法用于判断插件是否支持处理指定事件类型。如果支持,则返回true,否则返回false。
  • addEventListener(element: HTMLElement, eventName: string, handler: Function):该方法用于在指定元素上注册事件处理函数。它返回一个函数,用于注销事件处理函数。
  • addGlobalEventListener(element: string, eventName: string, handler: Function):该方法用于在全局范围内注册事件处理函数。它返回一个函数,用于注销事件处理函数。

下面是一个示例插件实现:

@Injectable()
export class CustomEventManagerPlugin implements EventManagerPlugin {
  supports(eventName: string): boolean {
    // 检查事件名称是否为 customEvent 后面会有具体实例
    return eventName === 'customEvent';
  }

  addEventListener(element: HTMLElement, eventName: string, handler: Function): Function {
    const customEventHandler = (event: CustomEvent) => {
      handler(event.xxx);
    };

    element.addEventListener(eventName, customEventHandler);

    // 返回一个函数 用于取消监听
    return () => {
      element.removeEventListener(eventName, customEventHandler);
    };
  }

  addGlobalEventListener(element: string, eventName: string, handler: Function): Function {
    throw new Error('Global event listeners are not supported for customEvent.');
  }
}

该插件实现了EventManagerPlugin接口,并定义了一个名为CustomEventManagerPlugin的类。supports方法检查事件类型是否为“customEvent”,如果是,则返回true,否则返回false。addEventListener方法使用element.addEventListener方法在元素上注册事件处理函数,并返回一个函数,用于注销事件处理函数。addGlobalEventListener方法抛出一个错误,因为它不需要支持全局事件处理函数。

  1. 在模块中providers数组中添加自定义事件管理插件

我们可以使用providers数组来声明插件。例如,如果我们在AppModule中声明了CustomEventManagerPlugin,代码如下:

@NgModule({
    declarations: [
      AppComponent
    ],
    imports: [
      BrowserModule
    ],
    providers: [
      {
        provide: EVENT_MANAGER_PLUGINS,
        useClass: CustomEventManagerPlugin,
        multi: true
      }
    ],
    bootstrap: [AppComponent]
  })
  export class AppModule { }

multi属性为true表示EVENT_MANAGER_PLUGINS令牌同时可以注册多个提供商

  1. 使用自定义事件管理插件示例

    @Component({ 
        selector: 'app-root',
        template: ` <button (customEvent)="onCustomEvent($event)">Trigger custom event</button> ` 
    }) 
    export class AppComponent { 
        onCustomEvent(data: any) { 
            console.log(`Custom event triggered with data: ${data}`); }
        }
    }
    

自定义事件管理插件案例一(实现runOutsideAngular)

我们可以编写事件插件,将某些事件处理函数放到Angular变更检测之外执行,因此不会触发Angular的变更检测,从而避免了额外的性能开销。

  1. 实现EventManagerPlugin接口

    import { Injectable } from '@angular/core';
    import { EventManager } from '@angular/platform-browser';
    
    export interface EventManagerPlugin {
      supports(eventName: string): boolean;
      addEventListener(element: HTMLElement, eventName: string, handler: Function): Function;
      addGlobalEventListener(element: string, eventName: string, handler: Function): Function;
    }
    @Injectable()
    export class ZoneOutsideEventPluginService implements EventManagerPlugin {
      addGlobalEventListener(element: string, eventName: string, handler: Function): Function {
          throw new Error('Global event listeners are not supported for .outside.');
      }
      manager!: EventManager;
    
      supports(eventName: string): boolean {
        // 以.outside结尾的事件都会激活该插件
        return eventName.endsWith('.outside');
      }
    
      addEventListener(element: HTMLElement, eventName: string, originalHandler: EventListener): Function {
        const [nativeEventName] = eventName.split('.');
        // 将事件处理函数放到Angular变更检测之外执行
        this.manager.getZone().runOutsideAngular(() => {
          element.addEventListener(nativeEventName, originalHandler);
        });
    
        return () => element.removeEventListener(nativeEventName, originalHandler);
      }
    }
    
    

    在上面的代码中,ZoneOutsideEventPluginService实现了EventManagerPlugin接口,然后重写了addEventListener方法。在addEventListener方法中,使用ngZone的runOutsideAngular方法将事件处理函数包装起来,简单来说就是将事件处理函数放到Angular变更检测之外执行并返回一个新的函数用于注销事件,然后实现了supports方法,当且仅当事件名称以.outside结尾该插件才会被激活

  2. 在模块中providers数组中添加自定义事件管理插件

    @NgModule({
      declarations: [
        AppComponent
      ],
      imports: [
        BrowserModule
      ],
      providers: [
        {
          provide: EVENT_MANAGER_PLUGINS,
          useClass: ZoneOutsideEventPluginService,
          multi: true
        }
      ],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    
  3. 几种使用方式

    • template语法
      <div #div (click.outside)="onClick()">click</div>
      
    • 使用@HostListener装饰器来监听宿主元素上的事件
      import { Component, HostListener } from '@angular/core';
      
      @Component({
             selector: 'app-example',
             template: '<button>Click me</button>'
      })
      export class ExampleComponent {
             @HostListener('click.outside')
             onClick() {
               // 执行一些操作
             }
      }
      
    • 使用内置的Renderer2来监听相应的事件
      import { Component, ElementRef, Renderer2 } from '@angular/core';
      
      @Component({
           selector: 'app-example',
           template: '<button>Click me</button>'
      })
      export class ExampleComponent {
           constructor(private elementRef: ElementRef, private renderer: Renderer2) {}
      
           ngAfterViewInit() {
             const button = this.elementRef.nativeElement.querySelector('button');
             this.renderer.listen(button, 'click.outside', () => {
               // 执行一些操作
             });
           }
      }
      
      
    • e