简述
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组件中使用@HostListener
或Renderer2
服务等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
令牌来获取事件管理插件,那么我们只需要注册这个提供商即可,创建自定义事件管理插件需要遵循以下步骤:
- 实现EventManagerPlugin接口。
- 在模块中providers数组中添加自定义事件管理插件。
下面我们将详细讨论每个步骤。
-
实现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
方法抛出一个错误,因为它不需要支持全局事件处理函数。
-
在模块中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
令牌同时可以注册多个提供商
-
使用自定义事件管理插件示例
@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的变更检测,从而避免了额外的性能开销。
-
实现
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结尾该插件才会被激活 -
在模块中providers数组中添加自定义事件管理插件
@NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule ], providers: [ { provide: EVENT_MANAGER_PLUGINS, useClass: ZoneOutsideEventPluginService, multi: true } ], bootstrap: [AppComponent] }) export class AppModule { }
-
几种使用方式
- 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
- template语法