实现“乞丐版”的DOM事件流机制

2020-01-16

概述

DOM事件流

先来简单回顾下什么是DOM事件流。

“DOM2级事件”规定的事件流包括三个阶段:事件捕获阶段==>处于目标阶段==>事件冒泡阶段。首先发生的是事件捕获阶段,为截获事件提供了机会。然后是实际的目标接收事件。最后一个阶段是冒泡阶段。用一张来自w3c的图片说明:

image

核心要素

从上图分析可知,要实现一个可用的DOM事件流机制,需要实现以下三个核心要素。

事件对象

事件对象用于保存事件的属性、状态和事件要传递的内容。

事件属性包括:

  • type:事件类型,用于不同事件的隔离
  • detail:事件要传递的内容
  • bubbles:是否支持冒泡
  • cancelable:是否支持取消事件默认行为

事件状态包括:

  • eventPhase:事件流当前所处的阶段
  • cancelBubble:是否取消了冒泡
  • defaultPrevented:是否取消了事件默认行为

除此之外,事件对象还需要包含以下方法:

  • stopPropagation:停止事件继续向上冒泡
  • preventDefault:阻止执行事件默认行为

事件目标

如上图中的windowdocument等对象,都属于事件目标,它们可以监听事件和分发事件。同时,还保存了它们的父级和子级的引用,以便可以在捕获和冒泡阶段,快速找到事件的传递路径。

事件目标需要包含以下方法:

  • addEventListener:监听事件,注册事件处理的回调函数
  • removeEventListener:移除事件监听
  • dispatchEvent:分发事件

事件中心

事件中心可以耦合到事件目标当中,也可以独立为一个模块。

事件中心用于保存监听的事件和调用事件回调函数,类似于Node.js中的EventEmitter对象。

稍微梳理一下,就应该是一个下面这样的模型:

image

源码实现

接下来,我们按照梳理的要素来逐步实现DOM事件流机制。

Event

实现Event对象很简单,这里直接贴代码。

class Event implements IEvent {
    readonly type: string;
    readonly bubbles: boolean;
    readonly cancelable: boolean;
    readonly eventPhase: EventPhase;
    readonly currentTarget: any;
    readonly target: any;
    readonly timeStamp: number;
    readonly detail: any;

    cancelBubble: boolean;
    defaultPrevented: boolean;

    constructor(type: string, eventInit?: TEventInit) {
        const options = eventInit || {};

        this.type = type;
        this.detail = options.detail;
        this.timeStamp = Date.now();
        this.bubbles = options.bubbles || false;
        this.cancelable = options.cancelable || false;
        this.target = null;
        this.currentTarget = null;

        this.eventPhase = EventPhase.NONE;
        this.cancelBubble = !this.bubbles;
        this.defaultPrevented = false;
    }

    preventDefault() {
        if (!this.cancelable) return;
        this.defaultPrevented = true;
    }

    stopPropagation() {
        this.cancelBubble = true;
    }
}

EventEmitter

EventEmitter很简单,只要通过一个listeners对象保存所有的事件监听,并在emit时执行监听对应的方法即可。需要注意的是:要对Event对象的eventPhase状态进行判断。下面是EventEmitter的核心代码:

class EventEmitter {
    // WeakMap: 在移除事件监听时,对应的 option 可以自动被垃圾回收
    private readonly options: WeakMap<TListener, TConfig="">;
    private readonly listeners: {
        [index:string]: Array<TListener>;
    };

    constructor() {
        this.options = new WeakMap();
        this.listeners = {};
    }

    on(event: string, listener: TListener) {
        if (!this.listeners[event]) {
            this.listeners[event] = [];
        }

        this.listeners[event].push(listener);
    }

    off(event: string, listener: TListener) {
        if (!this.listeners[event]) return;

        const listeners = this.listeners[event];
        if (listeners) {
            const index = listeners.indexOf(listener);
            if (index !== -1) listeners.splice(index, 1);
        }
    }

    emit(event: string, e: IEvent) {
        if (!this.listeners[event]) return;

        const listeners = this.listeners[event];
        if (listeners) {
            for (let i:number = 0; i < listeners.length; i++) {
                const listener = listeners[i];
                const option = this.options.get(listener) as TConfig;
                const { currentTarget, useCapture, isDefault } = option;
                if (
                    (isDefault &amp;&amp; !e.defaultPrevented) ||
                    (!isDefault &amp;&amp; (
                        (e.eventPhase === EventPhase.AT_TARGET) ||
                        (e.eventPhase === EventPhase.CAPTURING_PHASE &amp;&amp; useCapture) ||
                        (e.eventPhase === EventPhase.BUBBLING_PHASE &amp;&amp; !useCapture)
                    ))
                ) {
                    Object.defineProperty(e, 'currentTarget', {
                        configurable: true,
                        enumerable: true,
                        value: currentTarget,
                    });
                    listeners[i](e);
                }
            }
        }
    }
}

</TListener></TListener,>

事件对象

以下是EventTarget对象的核心代码。

class EventTarget {
    private readonly parent: EventTarget | null;
    private readonly children: Array<EventTarget>;

    private events: EventEmitter;

    constructor(parent?: EventTarget) {
        this.parent = parent || null;
        this.children = [];
        if (parent) parent.children.push(this);

        this.events = new EventEmitter();
    }

    addEventListener(type: string, listener: TListener, options?: TOption) {
        // 保存监听事件的当前对象为 currentTarget
        const listenerConfig: TConfig = { currentTarget: this, ...options };
        this.events.on(type, listener, listenerConfig);
    }

    removeEventListener(type: string, listener: TListener) {
        this.events.off(type, listener);
    }

    dispatchEvent(event: IEvent): void {
        event.target = this;

        let path: Array<EventTarget> = [];
        let node: EventTarget | null = this;
        while (node) {
            path.push(node);
            node = node.parent;
        }

        // capture
        event.eventPhase = EventPhase.CAPTURING_PHASE;
        for (let i = path.length - 1; i > 0; i--) {
            path[i].events.emit(event.type, event);
        }

        // target
        event.eventPhase = EventPhase.AT_TARGET;
        this.events.emit(event.type, event);

        // bubble
        event.eventPhase = EventPhase.BUBBLING_PHASE;
        for (let i = 1; i < path.length; i++) {
            if (event.cancelBubble) break;
            path[i].events.emit(event.type, event);
        }
    }
}

</EventTarget></EventTarget>

重点看下dispatchEvent方法的实现过程:

  1. 向上查找parent,直到parent === null,得到事件流的完整路径;
  2. 反向遍历数组(不包括数组的第一个元素),此过程即为事件的捕获阶段;
  3. 访问当前EventTarget,此过程即为事件的目标阶段;
  4. 正向遍历数组(不包括数组的第一个元素),此过程即为事件的冒泡阶段;

扩展阅读

原文链接:juejin.im

上一篇:「面试重点」聊一聊JS中call、apply、bind里的小心思
下一篇:CSS/CSS3 7 常见“坑”
相关教程
关注微信

扫码加入 JavaScript 社区

相关文章

首次访问,需要验证
微信扫码,关注即可
(仅需验证一次)

欢迎加入 JavaScript 社区

号内回复关键字:

回到顶部