在 Angular 中如何为同一个表达式绑定多个事件呢?如果我们这样做可能会是这样的:

<div><button (click, mouseover)="onClick()">Click me</button>
</div>复制代码

在继续分析绑定多个事件之前,我们先来分析一下,如果在模板中绑定一个事件如 click 事件,Angular 是如何工作的?

<div><button (click)="onClick()">Click me</button>
</div>复制代码

Angular 在解析 DOM 树的时候,对于事件绑定它会调用 DomRenderer 实例的 listen() 方法,进行事件绑定,listen() 方法具体实现如下:

// 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,我们先来会会它。

EventManager (事件管理器)

在 Angular 中所有的事件绑定都是由一个事件管理器来驱动,事件管理器本身由多个事件插件提供支持。Angular 中内置的事件插件如下:

  • KeyEventsPlugin - 处理键盘事件
  • HammerGesturesPlugin - 处理手势
  • DomEventsPlugin - 处理 DOM 事件

看完上面的内容,相信很多人也会有疑问 - 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, 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);}// 获取NgZonegetZone(): NgZone { return this._zone; }/** @internal */_findPluginFor(eventName: string): EventManagerPlugin {// 优先从_eventNameToPlugin对象中获取eventName对应的EventManagerPluginconst 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 抽象类。

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);};
}复制代码

时机已成熟,接下来我们开始实现上述的功能。

自定义插件

Step 1: Creating a new plugin

正如上面提到的,我们希望在我们的 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) 委派给此插件。

Step 2: Implementing the eventListeners

现在我们已经实现了supports() 方法,EventManager 将调用 plugin.addEventListener() 方法,因此插件需要实现 addEventListener() 方法,从而实现我们的自定义行为。我们的自定义行为很简单 - 为我们解析的eventArray 中的所有事件添加事件侦听器。

addEventListener

addEventListener(element: HTMLElement, eventName: string, handler: Function): Function {let zone = this.manager.getZone();let eventsArray = this.getMultiEventArray(eventName);// Entering back into angular to trigger changeDetectionlet 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

 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);})});
}复制代码

Step 3: Register plugin

import { EVENT_MANAGER_PLUGINS } from '@angular/platform-browser';@NgModule({...providers: [{ provide: EVENT_MANAGER_PLUGINS, useClass: MultiEventPlugin, multi: true }]
})
export class AppModule { }复制代码

完整示例

multi-event.plugin.ts

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 changeDetectionlet 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);});});}
}复制代码

app.component.ts

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');}
}复制代码

app.module.ts

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 { }复制代码

参考资源

  • Hacking Angular2: Binding Multiple DOM Events

Angular 4.x 事件管理器及自定义EventManagerPlugin相关推荐

  1. python异步框架twisted_详解Python的Twisted框架中reactor事件管理器的用法

    铺垫在大量的实践中,似乎我们总是通过类似的方式来使用异步编程: 监听事件 事件发生执行对应的回调函数 回调完成(可能产生新的事件添加进监听队列) 回到1,监听事件 因此我们将这样的异步模式称为Reac ...

  2. Django框架(16.Django中的模型类管理器以及自定义管理器)

    模型类.objects.all()->objects是一个什么东西呢? 答:objects是Django帮我自动生成的管理器对象,通过这个管理器可以实现对数据的查询. objects是model ...

  3. [cocos2dx笔记010]用于UI的事件管理器

    cocos2dx有一个编辑器:cocostudio.眼下来说,已经是比較好用了.仅仅要载入导出的资源.就能够用上了.省去手动搭建面的麻烦. 可是.非常多须要事件的地方,操作比較麻烦,所以这里提供一个事 ...

  4. phalcon:model 事件与事件管理器

    事件与事件管理器(Events and Events Manager)¶ Models allow you to implement events that will be thrown when p ...

  5. 利用windows事件管理器定位程序闪退错误

    问题说明: 1.公司的硬件新加了一个相机模块 需要从相机中获取图片,用以计算屈光值. 根据相机厂家提供的头文件SnCam.h,SnCam.lib,SnCam.dll封装了一个类文件用以读取相机的图片. ...

  6. [Unity] ACT 战斗系统学习 1:基于委托的事件管理器

    我觉得看视频太慢了,还是看别人源码更快-- 1.664235822/DarkSouls-Demo Github 源码: https://github.com/664235822/DarkSouls-D ...

  7. re管理器Java_自定义布局管理器-FormLayout

    第二部分:自定义布局管理器 在java.awt包与javax.swing包下有许多现成的布局类,比如BorderLayout.FlowLayout,还有较为复杂的.用于精确定位的布局类GridBagL ...

  8. Win 7 通过事件管理器查看计算机开机关机时间

    控制面板-管理工具-事件查看器 视图中开机来源:Kernel-General 事件ID:13 关机来源:Kernel-General 事件ID:12 转载于:https://www.cnblogs.c ...

  9. 事件管理器错误:来源DistributedCOM

    前言:windows10.事件查看器频繁捕捉到系统错误,遂查询下原因. 错误: 使用事件查看器查到错误如下: 应用程序-特定 权限设置并未向在应用程序容器 不可用 SID (不可用)中运行的地址 Lo ...

最新文章

  1. 【java】异常的分类
  2. tomcat7实战调优笔记
  3. .net生成图片验证码
  4. SQL语句对象化,先看示例代码.
  5. zcmu1209(dfs)
  6. zoj3806Incircle and Circumcircle
  7. 读取遥感图像中遇到的问题集锦
  8. Linux Signal及Golang中的信号处理
  9. C# list删除 另外list里面的元素_[Python]列表(list)操作
  10. Python 100 例 练习实例1
  11. 对付U盘病毒彻底免疫
  12. 基于SSM小说阅读网站设计带爬虫功能
  13. FZOJ P2109 【卡德加的兔子】
  14. blowfish算法c语言,使用Blowfish算法给文件加密
  15. 房地产开发商崩盘样本:楼盘捂了两年,欠40亿巨债
  16. java程序员首次使用mac M1
  17. 组合Combination 分布数组计算
  18. 给pdf、word、excel文件添加水印
  19. 软件经验|使用消费级无人机干测绘(一)影像数据获取
  20. 无人驾驶技术的7大典型应用场景

热门文章

  1. sketch钢笔工具_Sketch和Figma,不同的工具等于不同的结果
  2. 3 年前端面经和他在创业公司的成长历程
  3. 精通Spring Boot——第十一篇:使用自定义配置
  4. Linux文件和目录权限:chmod、更改所有者和所属组:chown,umask命令,隐藏权限:lsattr/chattr...
  5. offsetTop、offsetLeft、offsetWidth、offsetHeight、style中的样式
  6. yum搭建本地仓库、国内源、下载rpm包、源码安装
  7. [.net 面向对象程序设计深入](4)MVC 6 —— 谈谈MVC的版本变迁及新版本6.0发展方向...
  8. php 自动创建目录
  9. [转载].SSRAM、SDRAM和Flash简要介绍
  10. Channel Allocation HDU1373