Angular 4.x EventManager & Custom EventManagerPlugin
在 Angular 中如何为同一个表达式绑定多个事件呢?如果我们这样做可能会是这样的: <div> <button (click,mouSEOver)="onClick()">Click me</button> </div> 在继续分析绑定多个事件之前,我们先来分析一下,如果在模板中绑定一个事件如 click 事件,Angular 是如何工作的? <div> <button (click)="onClick()">Click me</button> </div> Angular 在解析 DOM 树的时候,对于事件绑定它会调用 // angular2/packages/platform-browser/src/dom/dom_renderer.ts class DefaultDomRenderer2 implements Renderer2 { .... listen(target: 'window'|'document'|'body'|any,event: string,callback: (event: any) => boolean): () => void { checkNoSyntheticProp(event,'listener'); if (typeof target === 'string') { return <() => void>this.eventManager.addGlobalEventListener( target,event,decoratePreventDefault(callback)); } return <() => void>this.eventManager.addEventListener( target,decoratePreventDefault(callback)) as() => void; } } 通过源码我们发现,不管走哪条分支,最终都是调用 EventManager (事件管理器)在 Angular 中所有的事件绑定都是由一个事件管理器来驱动,事件管理器本身由多个事件插件提供支持。Angular 中内置的事件插件如下:
看完上面的内容,相信很多人也会有疑问 - EventManager 到底是如何管理不同事件的呢?要揭开这背后的秘密,我们的唯一途径就是看源码,因为它是最诚实的,它对你毫无保留,此刻脑海中突然想起一首歌:
放松一下,马上回到正题 - EventManager 类: EventManager 类// angular2/packages/platform-browser/src/dom/events/event_manager.ts export class EventManager { // EventManagerPlugin列表 private _plugins: EventManagerPlugin[]; // 缓存已匹配的eventName与对应的插件 private _eventNameToPlugin = new Map<string,EventManagerPlugin>(); constructor( @Inject(EVENT_MANAGER_PLUGINS) plugins: EventManagerPlugin[],private _zone: NgZone) { plugins.forEach(p => p.manager = this); /** * {provide: EVENT_MANAGER_PLUGINS,useClass: DomEventsPlugin,multi: true},* {provide: EVENT_MANAGER_PLUGINS,useClass: KeyEventsPlugin,useClass: HammerGesturesPlugin,multi: true} * * slice(): 创建新的plugins数组 * reverse(): 让DomEventsPlugin插件作为列表最后一项,因为它能够处理所有的事件。 */ this._plugins = plugins.slice().reverse(); } // 获取能处理eventName的插件,并调用对应插件提供的addEventListener()方法 addEventListener(element: HTMLElement,eventName: string,handler: Function): Function { const plugin = this._findPluginFor(eventName); return plugin.addEventListener(element,eventName,handler); } // 获取能处理eventName的插件,并调用对应插件提供的addGlobalEventListener()方法 addGlobalEventListener(target: string,handler: Function): Function { const plugin = this._findPluginFor(eventName); return plugin.addGlobalEventListener(target,handler); } // 获取NgZone getZone(): NgZone { return this._zone; } /** @internal */ _findPluginFor(eventName: string): EventManagerPlugin { // 优先从_eventNameToPlugin对象中获取eventName对应的EventManagerPlugin const plugin = this._eventNameToPlugin.get(eventName); if (plugin) { return plugin; } // 遍历插件列表,判断当前插件是否支持eventName对应的事件名 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}`); } } 相关说明
EventManagerPlugin 抽象类export abstract class EventManagerPlugin { constructor(private _doc: any) {} manager: EventManager; // 判断是否支持eventName对应的事件 abstract supports(eventName: string): boolean; // 添加事件监听 abstract addEventListener(element: HTMLElement,handler: Function): Function; // 添加全局的事件监听 addGlobalEventListener(element: string,handler: Function): Function { const target: HTMLElement = getDOM().getGlobalEventTarget(this._doc,element); if (!target) { throw new Error(`Unsupported event target ${target} for event ${eventName}`); } return this.addEventListener(target,handler); }; }
时机已成熟,接下来我们开始实现上述的功能。 自定义插件Step 1: Creating a new plugin正如上面提到的,我们希望在我们的 Angular 模板上有多个事件绑定到同一个表达式: <div> <button (click,mouSEOver)="onClick()">Click me</button> </div> 如果是这样,我们的 supports() 函数的内部规则应该很清楚。我们需要一个字符串,其中有一个或多个逗号,分隔事件名称。当人们把一些愚蠢的东西放在 getMultiEventArray(eventName: string): string[] { return eventName.split(",") .filter((item,index): boolean => { return item && item != '' }) } supports(eventName: string): boolean { return this.getMultiEventArray(eventName).length > 1 } 这将允许 EventManager 将事件字符串如 (click,mouSEOver) 委派给此插件。 Step 2: Implementing the eventListeners现在我们已经实现了 addEventListeneraddEventListener(element: HTMLElement,handler: Function): Function { let zone = this.manager.getZone(); let eventsArray = this.getMultiEventArray(eventName); // Entering back into angular to trigger changeDetection let outsideHandler = (event: any) => { zone.runGuarded(() => handler(event)); }; // Executed outside of angular so that change detection is not // constantly triggered. let addAndRemoveHostListenersForOutsideEvents = () => { eventsArray.forEach((singleEventName: string) => { this.manager.addEventListener(element,singleEventName,outsideHandler); }); } return this.manager.getZone() .runOutsideAngular(addAndRemoveHostListenersForOutsideEvents); } addGlobalEventListeneraddGlobalEventListener(target: string,handler: Function): Function { let zone = this.manager.getZone(); let eventsArray = this.getMultiEventArray(eventName); let outsideHandler = (event: any) => zone.runGuarded(() => handler(event)); return this.manager.getZone().runOutsideAngular(() => { eventsArray.forEach((singleEventName: string) => { this.manager.addGlobalEventListener(target,outsideHandler); }) }); } Step 3: Register pluginimport { EVENT_MANAGER_PLUGINS } from '@angular/platform-browser'; @NgModule({ ... providers: [ { provide: EVENT_MANAGER_PLUGINS,useClass: MultiEventPlugin,multi: true } ] }) export class AppModule { } 完整示例multi-event.plugin.tsimport { Injectable,Inject } from '@angular/core'; import { EventManager,DOCUMENT,?d as EventManagerPlugin } from '@angular/platform-browser'; /** * Support Multi Event */ @Injectable() export class MultiEventPlugin extends EventManagerPlugin { manager: EventManager; constructor( @Inject(DOCUMENT) doc: any) { super(doc); } getMultiEventArray(eventName: string): string[] { return eventName.split(",") // click,mouSEOver => [click,mouSEOver] .filter((item,index): boolean => { return item && item != '' }) } supports(eventName: string): boolean { return this.getMultiEventArray(eventName).length > 1; } addEventListener(element: HTMLElement,handler: Function): Function { let zone = this.manager.getZone(); let eventsArray = this.getMultiEventArray(eventName); // Entering back into angular to trigger changeDetection let outsideHandler = (event: any) => { zone.runGuarded(() => handler(event)); }; // Executed outside of angular so that change detection is // not constantly triggered. let addAndRemoveHostListenersForOutsideEvents = () => { eventsArray.forEach((singleEventName: string) => { this.manager.addEventListener(element,outsideHandler); }); } return this.manager.getZone() .runOutsideAngular(addAndRemoveHostListenersForOutsideEvents); } addGlobalEventListener(target: string,outsideHandler); }); }); } } app.component.tsimport { Component } from '@angular/core'; @Component({ selector: 'exe-app',template: ` <div> <button (click,mouSEOver)="onClick()">Click me</button> </div> ` }) export class AppComponent { onClick() { console.log('Click'); } } app.module.tsimport { NgModule,CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { EVENT_MANAGER_PLUGINS } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { MultiEventPlugin } from './plugins/multi-event.plugin'; @NgModule({ imports: [BrowserModule],declarations: [AppComponent],bootstrap: [AppComponent],providers: [ { provide: EVENT_MANAGER_PLUGINS,multi: true } ],schemas: [CUSTOM_ELEMENTS_SCHEMA] }) export class AppModule { } 参考资源
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |