在 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 中内置的事件插件以下:缓存
看完上面的内容,相信不少人也会有疑问 - EventManager 究竟是如何管理不一样事件的呢?要揭开这背后的秘密,咱们的惟一途径就是看源码,由于它是最诚实的,它对你毫无保留,此刻脑海中忽然想起一首歌:angular2
美丽的神话app
解开我 最神秘的等待
星星坠落 风在吹动
终于再将你拥入怀中
….dom爱是心中惟一不变美丽的神话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}`);
}
}复制代码
_findPluginFor()
方法,查询对应的可以处理 eventName 对应的 EventManagerPlugin 插件对象。eventName
做为参数调用插件对象提供的 supports()
方法,判断当前是否可以处理 eventName
对应的事件。所以对于 EventManagerPlugin 插件对象,若是要声明可以处理某类事件,就须要在 supports()
方法中进行相应处理。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 { }复制代码