實現「乞丐版」的DOM事件流機制

概述

DOM事件流

先來簡單回顧下什麼是DOM事件流。node

「DOM2級事件」規定的事件流包括三個階段:事件捕獲階段==>處於目標階段==>事件冒泡階段。首先發生的是事件捕獲階段,爲截獲事件提供了機會。而後是實際的目標接收事件。最後一個階段是冒泡階段。用一張來自w3c的圖片說明:git

image

核心要素

從上圖分析可知,要實現一個可用的DOM事件流機制,須要實現如下三個核心要素。github

事件對象數組

事件對象用於保存事件的屬性、狀態和事件要傳遞的內容。dom

事件屬性包括:函數

  • type:事件類型,用於不一樣事件的隔離
  • detail:事件要傳遞的內容
  • bubbles:是否支持冒泡
  • cancelable:是否支持取消事件默認行爲

事件狀態包括:測試

  • eventPhase:事件流當前所處的階段
  • cancelBubble:是否取消了冒泡
  • defaultPrevented:是否取消了事件默認行爲

除此以外,事件對象還須要包含如下方法:ui

  • stopPropagation:中止事件繼續向上冒泡
  • preventDefault:阻止執行事件默認行爲

事件目標this

如上圖中的windowdocument等對象,都屬於事件目標,它們能夠監聽事件和分發事件。同時,還保存了它們的父級和子級的引用,以即可以在捕獲和冒泡階段,快速找到事件的傳遞路徑。spa

事件目標須要包含如下方法:

  • 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 && !e.defaultPrevented) ||
                    (!isDefault && (
                        (e.eventPhase === EventPhase.AT_TARGET) ||
                        (e.eventPhase === EventPhase.CAPTURING_PHASE && useCapture) ||
                        (e.eventPhase === EventPhase.BUBBLING_PHASE && !useCapture)
                    ))
                ) {
                    Object.defineProperty(e, 'currentTarget', {
                        configurable: true,
                        enumerable: true,
                        value: currentTarget,
                    });
                    listeners[i](e);
                }
            }
        }
    }
}
複製代碼

事件對象

如下是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);
        }
    }
}
複製代碼

重點看下dispatchEvent方法的實現過程:

  1. 向上查找parent,直到parent === null,獲得事件流的完整路徑;
  2. 反向遍歷數組(不包括數組的第一個元素),此過程即爲事件的捕獲階段;
  3. 訪問當前EventTarget,此過程即爲事件的目標階段;
  4. 正向遍歷數組(不包括數組的第一個元素),此過程即爲事件的冒泡階段;

擴展閱讀

相關文章
相關標籤/搜索