7

以前对事件都是能用就行,本周对事件进行了一下较全面的学习, 在这里记录一下。

什么是事件

事件可以理解为行为,如对button的点击,事件的本质是一个函数,他接收一个event对象。
每个事件会产生一个Event对象,Event 对象代表事件的状态,比如事件在其中发生的元素、键盘按键的状态、鼠标的位置、鼠标按钮的状态。

事件传播机制

事件的传播机制可以分为冒泡捕获

冒泡

官方的定义就是从最特定的事件目标到最不特定的事件目标。

意思就是说,假如用户单击了一个元素,该元素拥有一个click事件,那么同样的事件也将会被它的祖先触发,这个事件从该元素开始一直冒泡到DOM树的最上层,这一过程称为事件冒泡

捕获

事件捕获和事件是相反的,也就是说,当用户触发了一个事件的时候,这个事件是从DOM树的最上层开始触发一直到捕获到事件源。

事件流

事件流是事件传播机制的标准

由于微软和网景乱搞,后来必须要为事件传播机制,制定一个标准,因为事件捕获是网景公司开发出来的,而事件冒泡是由微软公司开发出来的,它们都想要自己的技术成为标准,所以导致这两个公司老是干架,制定标准的人为了不让它们干架,所以研发了事件流。

事件流的使用

事件流的使用就是我们常用的addEventListener了,标准用法如下

dom对象.addEventListener(事件类型, 回调函数, 事件机制)这里的事件类型表示你要使用哪种事件类型比如click, 回调函数里面写着触发此事件你要做什么事情, 事件机制分为冒泡和捕获,如果为false表示事件冒泡,为true表示事件捕获

发现自己以前一直使用的都是不标准的写法

dom对象.attachEvent(eventType, fn)第一个参数表示事件类型,第二个是回调,这种的事件传播机制是冒泡

关于js的事件就到这里了,如果想要了解js的兼容写法等可以查看这篇文章

angular事件

Angular 组件和 DOM 元素通过事件与外部进行通信, Angular 事件绑定语法对于组件和 DOM 元素来说是相同的 - (eventName)="expression" :

<button (click)="onClick()">Click</button>

angular 事件特点:

Angular 支持 DOM 事件冒泡机制,但不支持自定义事件的冒泡

angular是如何处理事件的

下面我们来看看angular是如何处理事件的,下面的源码阅读过程来自Angular 4.x EventManager & Custom EventManagerPlugin

listen

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 类

// 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 抽象类。

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);
  };
}

图片描述

用一个流程图表示

GH9UMT.jpg

自定义事件处理

了解了事件的处理过程,是否疑惑我们是否可以自定义事件处理过程?是的我们可以,通过自定义EventManagerPlugin的方式,当然这应该是相当高端的操作了,有兴趣的可以看这篇文章

参考文章

javascript event(事件对象)详解
Angular 4.x EventManager & Custom EventManagerPlugin
Angular 4.x Events Bubbling


笙歌会停
1k 声望45 粉丝

代码成就万世基积沙镇海 梦想永在凌云意意气风发