DomEvent
# 概述
DomEvent
是Leaflet库中用于处理 DOM 事件的核心模块 DomEvent
的实现。它提供了一套跨浏览器兼容的事件绑定、解绑、传播控制等功能
# 源码分析
# 源码实现
DomEvent
的源码实现如下:
export function on(obj, types, fn, context) {
if (types && typeof types === "object") {
for (var type in types) {
addOne(obj, type, types[type], fn);
}
} else {
types = Util.splitWords(types);
for (var i = 0, len = types.length; i < len; i++) {
addOne(obj, types[i], fn, context);
}
}
return this;
}
var eventsKey = "_leaflet_events";
export function off(obj, types, fn, context) {
if (arguments.length === 1) {
batchRemove(obj);
delete obj[eventsKey];
} else if (types && typeof types === "object") {
for (var type in types) {
removeOne(obj, type, types[type], fn);
}
} else {
types = Util.splitWords(types);
if (arguments.length === 2) {
batchRemove(obj, function (type) {
return Util.indexOf(types, type) !== -1;
});
} else {
for (var i = 0, len = types.length; i < len; i++) {
removeOne(obj, types[i], fn, context);
}
}
}
return this;
}
function batchRemove(obj, filterFn) {
for (var id in obj[eventsKey]) {
var type = id.split(/\d/)[0];
if (!filterFn || filterFn(type)) {
removeOne(obj, type, null, null, id);
}
}
}
var mouseSubst = {
mouseenter: "mouseover",
mouseleave: "mouseout",
wheel: !("onwheel" in window) && "mousewheel",
};
function addOne(obj, type, fn, context) {
var id = type + Util.stamp(fn) + (context ? "_" + Util.stamp(context) : "");
if (obj[eventsKey] && obj[eventsKey][id]) {
return this;
}
var handler = function (e) {
return fn.call(context || obj, e || window.event);
};
var originalHandler = handler;
if (!Browser.touchNative && Browser.pointer && type.indexOf("touch") === 0) {
// Needs DomEvent.Pointer.js
handler = addPointerListener(obj, type, handler);
} else if (Browser.touch && type === "dblclick") {
handler = addDoubleTapListener(obj, handler);
} else if ("addEventListener" in obj) {
if (
type === "touchstart" ||
type === "touchmove" ||
type === "wheel" ||
type === "mousewheel"
) {
obj.addEventListener(
mouseSubst[type] || type,
handler,
Browser.passiveEvents ? { passive: false } : false
);
} else if (type === "mouseenter" || type === "mouseleave") {
handler = function (e) {
e = e || window.event;
if (isExternalTarget(obj, e)) {
originalHandler(e);
}
};
obj.addEventListener(mouseSubst[type], handler, false);
} else {
obj.addEventListener(type, originalHandler, false);
}
} else {
obj.attachEvent("on" + type, handler);
}
obj[eventsKey] = obj[eventsKey] || {};
obj[eventsKey][id] = handler;
}
function removeOne(obj, type, fn, context, id) {
id = id || type + Util.stamp(fn) + (context ? "_" + Util.stamp(context) : "");
var handler = obj[eventsKey] && obj[eventsKey][id];
if (!handler) {
return this;
}
if (!Browser.touchNative && Browser.pointer && type.indexOf("touch") === 0) {
removePointerListener(obj, type, handler);
} else if (Browser.touch && type === "dblclick") {
removeDoubleTapListener(obj, handler);
} else if ("removeEventListener" in obj) {
obj.removeEventListener(mouseSubst[type] || type, handler, false);
} else {
obj.detachEvent("on" + type, handler);
}
obj[eventsKey][id] = null;
}
export function stopPropagation(e) {
if (e.stopPropagation) {
e.stopPropagation();
} else if (e.originalEvent) {
// In case of Leaflet event.
e.originalEvent._stopped = true;
} else {
e.cancelBubble = true;
}
return this;
}
export function disableScrollPropagation(el) {
addOne(el, "wheel", stopPropagation);
return this;
}
export function disableClickPropagation(el) {
on(el, "mousedown touchstart dblclick contextmenu", stopPropagation);
el["_leaflet_disable_click"] = true;
return this;
}
export function preventDefault(e) {
if (e.preventDefault) {
e.preventDefault();
} else {
e.returnValue = false;
}
return this;
}
export function stop(e) {
preventDefault(e);
stopPropagation(e);
return this;
}
export function getPropagationPath(ev) {
if (ev.composedPath) {
return ev.composedPath();
}
var path = [];
var el = ev.target;
while (el) {
path.push(el);
el = el.parentNode;
}
return path;
}
export function getMousePosition(e, container) {
if (!container) {
return new Point(e.clientX, e.clientY);
}
var scale = getScale(container),
offset = scale.boundingClientRect; // left and top values are in page scale (like the event clientX/Y)
return new Point(
(e.clientX - offset.left) / scale.x - container.clientLeft,
(e.clientY - offset.top) / scale.y - container.clientTop
);
}
var wheelPxFactor =
Browser.linux && Browser.chrome
? window.devicePixelRatio
: Browser.mac
? window.devicePixelRatio * 3
: window.devicePixelRatio > 0
? 2 * window.devicePixelRatio
: 1;
export function getWheelDelta(e) {
return Browser.edge
? e.wheelDeltaY / 2 // Don't trust window-geometry-based delta
: e.deltaY && e.deltaMode === 0
? -e.deltaY / wheelPxFactor // Pixels
: e.deltaY && e.deltaMode === 1
? -e.deltaY * 20 // Lines
: e.deltaY && e.deltaMode === 2
? -e.deltaY * 60 // Pages
: e.deltaX || e.deltaZ
? 0 // Skip horizontal/depth wheel events
: e.wheelDelta
? (e.wheelDeltaY || e.wheelDelta) / 2 // Legacy IE pixels
: e.detail && Math.abs(e.detail) < 32765
? -e.detail * 20 // Legacy Moz lines
: e.detail
? (e.detail / -32765) * 60 // Legacy Moz pages
: 0;
}
export function isExternalTarget(el, e) {
var related = e.relatedTarget;
if (!related) {
return true;
}
try {
while (related && related !== el) {
related = related.parentNode;
}
} catch (err) {
return false;
}
return related !== el;
}
export { on as addListener };
export { off as removeListener };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# 源码详细解析
- 事件绑定与解绑
on(obj, types, fn, context)
- 作用:为对象(通常是 DOM 元素)绑定事件监听器。
- 实现:
- 如果
types
是一个对象,则遍历对象,为每个事件类型绑定对应的处理函数。 - 如果
types
是字符串,则通过Util.splitWords
将其拆分为多个事件类型,依次绑定。 - 调用
addOne
函数实际绑定事件。
- 如果
- 返回值:返回
this
,支持链式调用。
off(obj, types, fn, context)
- 作用:移除对象的事件监听器。
- 实现:
- 如果只有
obj
参数,则移除所有事件监听器。 - 如果
types
是一个对象,则遍历对象,移除每个事件类型对应的处理函数。 - 如果
types
是字符串,则通过Util.splitWords
拆分为多个事件类型,依次移除。 - 调用
removeOne
或batchRemove
函数实际移除事件。
- 如果只有
- 返回值:返回
this
,支持链式调用。
addOne(obj, type, fn, context)
- 作用:为对象绑定单个事件监听器。
- 实现:
- 生成唯一的事件 ID,用于存储和查找事件处理函数。
- 根据事件类型和浏览器特性,选择合适的方式绑定事件(如
addEventListener
或attachEvent
)。 - 处理特殊事件(如
touch
、dblclick
、mouseenter
等)的兼容性。 - 将事件处理函数存储在
obj[eventsKey]
中。
removeOne(obj, type, fn, context, id)
- 作用:移除对象的单个事件监听器。
- 实现:
- 根据事件 ID 查找并移除事件处理函数。
- 处理特殊事件(如
touch
、dblclick
)的兼容性。 - 从
obj[eventsKey]
中移除事件处理函数。
batchRemove(obj, filterFn)
- 作用:批量移除对象的事件监听器。
- 实现:
- 遍历
obj[eventsKey]
,移除符合条件的事件监听器。 - 如果提供了
filterFn
,则只移除匹配的事件类型。
- 遍历
- 事件传播控制
stopPropagation(e)
- 作用:阻止事件传播。
- 实现:
- 优先调用
e.stopPropagation()
。 - 如果事件是 Leaflet 自定义事件,则设置
e.originalEvent._stopped
。 - 回退到设置
e.cancelBubble = true
。
- 优先调用
disableScrollPropagation(el)
作用:阻止滚动事件的传播。 实现:绑定wheel
事件,调用stopPropagation
。disableClickPropagation(el)
作用:阻止点击事件的传播。
实现:绑定
mousedown
、touchstart
、dblclick
、contextmenu
事件,调用stopPropagation
。preventDefault(e)
- 作用:阻止事件的默认行为。
- 实现:
- 优先调用
e.preventDefault()
。 - 回退到设置
e.returnValue = false
。
stop(e)
- 作用:同时阻止事件传播和默认行为。
- 实现:调用
preventDefault
和stopPropagation
。
- 事件工具函数
getPropagationPath(ev)
- 作用:获取事件的传播路径。
- 实现:
- 优先使用
ev.composedPath()
。 - 回退到手动遍历
parentNode
获取路径。
getMousePosition(e, container)
- 作用:获取鼠标事件在指定容器中的位置。
- 实现:
- 如果未提供容器,则返回基于窗口的坐标。
- 否则,根据容器的缩放比例和偏移量计算相对坐标。
getWheelDelta(e)
- 作用:获取滚轮事件的滚动量。
- 实现:
- 根据浏览器和操作系统特性,计算滚轮事件的
deltaY
值。 - 处理不同滚动模式(像素、行、页)的兼容性。
isExternalTarget(el, e)
- 作用:判断事件的目标是否在指定元素外部。
- 实现:
- 遍历
e.relatedTarget
的父节点,判断是否与el
相同。 - 如果
relatedTarget
不存在,则返回true
。
- 遍历
- 兼容性处理
mouseSubst
- 作用:处理鼠标事件的浏览器兼容性。
- 实现:
- 将
mouseenter
和mouseleave
分别映射为mouseover
和mouseout
。 - 将
wheel
事件映射为mousewheel
(如果浏览器不支持wheel
)。
- 将
wheelPxFactor
- 作用:根据浏览器和操作系统特性,计算滚轮事件的缩放因子。
- 实现:
- 针对不同浏览器和操作系统,设置不同的缩放因子。
- 导出与别名
export {on as addListener};
export {off as removeListener};
1
2
2
- 作用:为
on
和off
提供别名addListener
和removeListener
。 - 意义:兼容旧版 API 或提供更直观的命名。
# 总结
DomEvent
是 Leaflet 中用于处理 DOM 事件的核心模块,具有以下特点:
- 跨浏览器兼容性:通过检测浏览器特性,选择合适的方式绑定和解绑事件。
- 高效的事件管理:使用唯一 ID 存储事件处理函数,支持批量移除和链式调用。
- 灵活的事件控制:提供阻止事件传播和默认行为的工具函数。
- 特殊事件处理:支持
touch
、wheel
、mouseenter
等特殊事件的兼容性处理。
这些功能为 Leaflet 的交互性(如地图拖动、缩放、点击等)提供了坚实的基础。
编辑 (opens new window)
上次更新: 2025/03/20, 00:55:00