概述
DOM事件流
先来简单回顾下什么是DOM事件流。
“DOM2级事件”规定的事件流包括三个阶段:事件捕获阶段==>处于目标阶段==>事件冒泡阶段。首先发生的是事件捕获阶段,为截获事件提供了机会。然后是实际的目标接收事件。最后一个阶段是冒泡阶段。用一张来自w3c的图片说明:
核心要素
从上图分析可知,要实现一个可用的DOM事件流机制,需要实现以下三个核心要素。
事件对象
事件对象用于保存事件的属性、状态和事件要传递的内容。
事件属性包括:
- type:事件类型,用于不同事件的隔离
- detail:事件要传递的内容
- bubbles:是否支持冒泡
- cancelable:是否支持取消事件默认行为
事件状态包括:
- eventPhase:事件流当前所处的阶段
- cancelBubble:是否取消了冒泡
- defaultPrevented:是否取消了事件默认行为
除此之外,事件对象还需要包含以下方法:
- stopPropagation:停止事件继续向上冒泡
- preventDefault:阻止执行事件默认行为
事件目标
如上图中的window、document等对象,都属于事件目标,它们可以监听事件和分发事件。同时,还保存了它们的父级和子级的引用,以便可以在捕获和冒泡阶段,快速找到事件的传递路径。
事件目标需要包含以下方法:
- addEventListener:监听事件,注册事件处理的回调函数
- removeEventListener:移除事件监听
- dispatchEvent:分发事件
事件中心
事件中心可以耦合到事件目标当中,也可以独立为一个模块。
事件中心用于保存监听的事件和调用事件回调函数,类似于Node.js中的EventEmitter对象。
稍微梳理一下,就应该是一个下面这样的模型:
源码实现
接下来,我们按照梳理的要素来逐步实现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方法的实现过程:
- 向上查找
parent,直到parent === null,得到事件流的完整路径; - 反向遍历数组(不包括数组的第一个元素),此过程即为事件的捕获阶段;
- 访问当前
EventTarget,此过程即为事件的目标阶段; - 正向遍历数组(不包括数组的第一个元素),此过程即为事件的冒泡阶段;