在 Angular 中如何爲同一個表達式綁定多個事件呢?若是咱們這樣作可能會是這樣的:html
<div> <button (click, mouseover)="onClick()">Click me</button> </div>
在繼續分析綁定多個事件以前,咱們先來分析一下,若是在模板中綁定一個事件如 click 事件,Angular 是如何工做的?typescript
<div> <button (click)="onClick()">Click me</button> </div>
Angular 在解析 DOM 樹的時候,對於事件綁定它會調用 DomRenderer
實例的 listen()
方法,進行事件綁定,listen()
方法具體實現以下:bootstrap
// 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, event, decoratePreventDefault(callback)) as() => void; } }
經過源碼咱們發現,無論走哪條分支,最終都是調用 this.eventManager
對象的方法設置事件監聽。這裏的 this.eventManager
是什麼?它是 Angular 中的事件管理器 EventManager
,咱們先來會會它。數組
在 Angular 中全部的事件綁定都是由一個事件管理器來驅動,事件管理器自己由多個事件插件提供支持。Angular 中內置的事件插件以下:緩存
KeyEventsPlugin - 處理鍵盤事件angular2
HammerGesturesPlugin - 處理手勢app
DomEventsPlugin - 處理 DOM 事件dom
看完上面的內容,相信不少人也會有疑問 - EventManager 究竟是如何管理不一樣事件的呢?要揭開這背後的祕密,咱們的惟一途徑就是看源碼,由於它是最誠實的,它對你毫無保留,此刻腦海中忽然想起一首歌:ide
美麗的神話函數
解開我 最神祕的等待
星星墜落 風在吹動
終於再將你擁入懷中
….愛是心中惟一不變美麗的神話
放鬆一下,立刻回到正題 - 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, multi: true}, * {provide: EVENT_MANAGER_PLUGINS, 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, eventName: string, handler: Function): Function { const plugin = this._findPluginFor(eventName); return plugin.addGlobalEventListener(target, eventName, 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}`); } }
在 addEventListener() 或 addGlobalEventListener() 方法內部都會調用 _findPluginFor()
方法,查詢對應的可以處理 eventName 對應的 EventManagerPlugin 插件對象。
_findPluginFor() 方法中,會遍歷插件列表,而後以 eventName
做爲參數調用插件對象提供的 supports()
方法,判斷當前是否可以處理 eventName
對應的事件。所以對於 EventManagerPlugin 插件對象,若是要聲明可以處理某類事件,就須要在 supports()
方法中進行相應處理。
DomEventsPlugin 插件做爲列表最後一項,由於它可以處理全部的事件。
KeyEventsPlugin、HammerGesturesPlugin、DomEventsPlugin 插件類都繼承於 EventManagerPlugin 抽象類。
export abstract class EventManagerPlugin { constructor(private _doc: any) {} manager: EventManager; // 判斷是否支持eventName對應的事件 abstract supports(eventName: string): boolean; // 添加事件監聽 abstract addEventListener(element: HTMLElement, eventName: string, handler: Function): Function; // 添加全局的事件監聽 addGlobalEventListener(element: string, eventName: 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, eventName, handler); }; }
時機已成熟,接下來咱們開始實現上述的功能。
正如上面提到的,咱們但願在咱們的 Angular 模板上有多個事件綁定到同一個表達式:
<div> <button (click, mouseover)="onClick()">Click me</button> </div>
若是是這樣,咱們的 supports() 函數的內部規則應該很清楚。咱們須要一個字符串,其中有一個或多個逗號,分隔事件名稱。當人們把一些愚蠢的東西放在(,click)
中時,咱們也應該處理。因此咱們的 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) 委派給此插件。
如今咱們已經實現了supports()
方法,EventManager 將調用 plugin.addEventListener()
方法,所以插件須要實現 addEventListener()
方法,從而實現咱們的自定義行爲。咱們的自定義行爲很簡單 - 爲咱們解析的eventArray 中的全部事件添加事件偵聽器。
addEventListener(element: HTMLElement, eventName: string, 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); }
addGlobalEventListener(target: string, eventName: 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, singleEventName, outsideHandler); }) }); }
import { EVENT_MANAGER_PLUGINS } from '@angular/platform-browser'; @NgModule({ ... providers: [ { provide: EVENT_MANAGER_PLUGINS, useClass: MultiEventPlugin, multi: true } ] }) export class AppModule { }
import { 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, eventName: string, 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); } addGlobalEventListener(target: string, eventName: 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, singleEventName, outsideHandler); }); }); } }
import { 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'); } }
import { 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, useClass: MultiEventPlugin, multi: true } ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) export class AppModule { }