先來簡單回顧下什麼是DOM事件流。node
「DOM2級事件」規定的事件流包括三個階段:事件捕獲階段==>處於目標階段==>事件冒泡階段。首先發生的是事件捕獲階段,爲截獲事件提供了機會。而後是實際的目標接收事件。最後一個階段是冒泡階段。用一張來自w3c的圖片說明:git
從上圖分析可知,要實現一個可用的DOM事件流機制,須要實現如下三個核心要素。github
事件對象數組
事件對象用於保存事件的屬性、狀態和事件要傳遞的內容。dom
事件屬性包括:函數
事件狀態包括:測試
除此以外,事件對象還須要包含如下方法:ui
事件目標this
如上圖中的window
、document
等對象,都屬於事件目標,它們能夠監聽事件和分發事件。同時,還保存了它們的父級和子級的引用,以即可以在捕獲和冒泡階段,快速找到事件的傳遞路徑。spa
事件目標須要包含如下方法:
事件中心
事件中心能夠耦合到事件目標當中,也能夠獨立爲一個模塊。
事件中心用於保存監聽的事件和調用事件回調函數,相似於Node.js
中的EventEmitter
對象。
稍微梳理一下,就應該是一個下面這樣的模型:
接下來,咱們按照梳理的要素來逐步實現DOM事件流機制。
實現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
很簡單,只要經過一個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
方法的實現過程:
parent
,直到parent === null
,獲得事件流的完整路徑;EventTarget
,此過程即爲事件的目標階段;