事件系统
目的与范围
事件系统提供了发布-订阅(pub-sub)event 架构,为 Leaflet 中的所有用户交互和组件通信提供支持。本文档涵盖 Evented 基类、event 注册(on、once、off)、event 触发与传播、listener 管理以及 event parent 关系。
关于 DOM event 处理和浏览器 event 规范化的信息,请参阅 DOM Utilities and Event Handling。关于整体 class 继承系统,请参阅 Utilities and Class System。
概述
Evented Class 继承自 Class,为 Map、Marker 和 Layer 等组件提供基于 event 的功能。该系统支持:
- 每个 event 类型的多个 listener
- 自定义执行上下文
- 空格分隔的 event 类型(例如
'click dblclick') - 基于对象的 event 注册(例如
{click: fn1, move: fn2}) - 通过
once()实现的一次性 listener - 通过 parent 对象进行 event 传播
- 在 event 触发期间移除 listener(安全的可重入性)
图 1:Evented Class 架构
Event 注册
使用 on() 添加 Listener
on() 方法使用灵活的语法注册 event listener:
单个 event 类型与函数:
map.on('click', function(e) {
console.log(e.latlng);
});多个空格分隔的类型:
map.on('click dblclick', handler);基于对象的注册:
map.on({
click: onClickHandler,
move: onMoveHandler
});使用自定义上下文:
map.on('click', this.handleClick, this);实现处理输入并委托给 _on():
| 输入类型 | 处理方式 | 示例 |
|---|---|---|
| Object | 遍历条目,为每个调用 _on() | {click: fn1, move: fn2} |
| String | 按空白字符分割,为每个调用 _on() | 'click dblclick' |
| 单个字符串 | 直接调用 _on() | 'click' |
核心注册:_on()
_on() 方法执行实际的 listener 注册:
- 验证:检查已移除的鼠标事件并验证函数类型
- 重复预防:使用
_listens()防止重复注册 - 上下文优化:如果
context === this,则将context设置为undefined以减少内存 - Listener 对象创建:创建
{fn, ctx, once?}结构 - 存储:添加到
this._events[type]数组
图 2:Listener 注册流程
一次性 Listener:once()
once() 方法注册在首次执行后自动移除的 listener:
map.once('load', function() {
console.log('Map loaded');
});实现委托给带有 _once = true 标志的 _on()。在 fire() 期间,带有 once: true 的 listener 会在调用前通过 this.off() 移除。
移除 Listener:off()
off() 方法支持多种移除模式:
| 模式 | 行为 | 示例 |
|---|---|---|
| 无参数 | 移除所有 listener | obj.off() |
| 仅类型 | 移除该类型的所有 listener | obj.off('click') |
| 类型 + 函数 | 移除特定 listener | obj.off('click', fn) |
| 类型 + 函数 + 上下文 | 移除带上下文的 listener | obj.off('click', fn, ctx) |
| Object | 移除多对类型/listener | obj.off({click: fn1, move: fn2}) |
可重入安全性: 如果在 fire() 期间移除 listener,它们会被替换为 Util.falseFn 空操作,并且数组会被复制以防止迭代器损坏。
图 3:off() 方法决策树
Event 触发
基本 Event 触发:fire()
fire() 方法使用 event 对象触发已注册的 listener:
map.fire('click', {
latlng: L.latLng(51.5, -0.09),
originalEvent: domEvent
});Event 对象结构:
| 属性 | 描述 | 来源 |
|---|---|---|
type | Event 类型字符串 | 来自 fire() 参数 |
target | 触发 event 的对象 | this |
sourceTarget | 原始源对象 | data.sourceTarget 或 this |
propagatedFrom | 传播 event 的 parent | 在传播期间设置 |
...data | 自定义属性 | 从 data 参数展开 |
触发流程
图 4:Event 触发序列
可重入保护
_firingCount 属性跟踪嵌套的 fire() 调用。当 listener 修改 listener 数组时(在 fire() 期间通过 off()):
- 修改后的 listener 的
fn被替换为Util.falseFn - 在 splicing 之前复制
_events[type]数组 - 防止迭代器损坏并确保可预测的执行顺序
Event 传播
Event Parent 关系
Leaflet 使用 event parent 在组件层次结构中向上传播 event。例如,Marker 可以将其 event 传播到包含它的 FeatureGroup,后者再传播到 Map。
图 5:Event 传播层次结构
管理 Event Parent
添加 parent:
marker.addEventParent(featureGroup);移除 parent:
marker.removeEventParent(featureGroup);Parent 使用 Util.stamp() 进行唯一标识,存储在 _eventParents 中:
this._eventParents[Util.stamp(obj)] = obj;传播实现
_propagateEvent() 方法在所有 parent 上触发 event:
- 遍历
Object.values(this._eventParents ?? {}) - 对于每个 parent,调用
parent.fire(type, modifiedEvent, true) - 修改后的 event 包括:
propagatedFrom:传播的直接子对象- 所有原始 event 属性
target更新为 parent(在fire()中完成)
图 6:传播过程中的 Event 对象转换
查询 Listener:listens()
listens() 方法检查 listener 是否存在,可选择是否包含传播:
基本用法:
if (map.listens('click')) {
// 有 click listener
}检查特定函数:
if (map.listens('click', myHandler)) {
// myHandler 已注册用于 click
}检查是否包含传播:
if (marker.listens('click', true)) {
// Marker 或其 parent 有 click listener
}方法签名:
| 签名 | 返回 | 描述 |
|---|---|---|
listens(type) | boolean | 该类型是否有任何 listener |
listens(type, fn, context) | boolean | 是否有特定的 listener |
listens(type, propagate) | boolean | 是否有 listener(如果为 true 则检查 parent) |
listens(type, fn, context, propagate) | boolean | 包含传播的完整检查 |
实现逻辑
图 7:listens() 实现流程
内部架构
数据结构
Evented Class 维护三个主要内部属性:
_events 对象:
_events = {
'click': [
{fn: handler1, ctx: undefined, once: false},
{fn: handler2, ctx: customObj, once: false},
{fn: handler3, ctx: undefined, once: true}
],
'move': [
{fn: moveHandler, ctx: undefined, once: false}
]
}_eventParents 对象:
_eventParents = {
123: featureGroupInstance, // 键是 Util.stamp(obj)
456: mapInstance
}_firingCount 数字: 跟踪嵌套的 fire() 调用以处理可重入性。进入时递增,退出时递减。
Listener 查找:_listens()
_listens() 方法执行核心 listener 搜索:
- 如果没有
_events或该类型没有 listener,则返回false - 如果没有请求特定函数,则返回
true(作为!!listeners.length) - 优化上下文:如果
context === this,则将context设置为undefined - 使用
Array.findIndex()定位匹配的{fn, ctx}对 - 返回索引号或
false
这个返回值(数字 vs false)被 _off() 用于 splice 数组。
上下文优化
为了减少内存占用,Leaflet 将 context === this 视为 context = undefined:
if (context === this) {
context = undefined;
}这个优化出现在 _on() 和 _listens() 中,确保注册和查找期间的一致匹配。
常见使用模式
Map Event 处理
const map = L.map('map');
map.on('click', function(e) {
console.log('Clicked at', e.latlng);
});
map.on('zoom', function(e) {
console.log('Zoom level:', map.getZoom());
});Layer Event 冒泡
Layer 自动将 map 设置为 event parent,启用传播:
const marker = L.marker([51.5, -0.09]).addTo(map);
// 在 map 上监听 marker event
map.on('click', function(e) {
if (e.sourceTarget === marker) {
console.log('Marker clicked');
}
});
// 触发传播:marker → map
marker.fire('click', {}, true);内存管理
移除 listener 以防止内存泄漏:
function onClick(e) { /* ... */ }
map.on('click', onClick);
// 稍后,完成时:
map.off('click', onClick);
// 或移除所有 listener:
map.off();临时 Listener
对一次性 event 使用 once():
map.once('load', function() {
console.log('Initial load complete');
});
// 或在 handler 内移除:
map.on('click', function handler(e) {
console.log('First click');
map.off('click', handler);
});与 Class System 集成
Evented 继承自 Class,继承了 option system 和初始化钩子。需要 event 功能的 Class 只需继承 Evented:
import {Evented} from './core/Events.js';
class MyComponent extends Evented {
initialize(options) {
// 组件初始化
}
doSomething() {
this.fire('something', {data: 'value'});
}
}
const component = new MyComponent();
component.on('something', (e) => {
console.log(e.data);
});
component.doSomething(); // 触发 event性能考虑
Event 注册性能
- 对象语法优化:使用
on({type1: fn1, type2: fn2})时,跳过空格分隔的类型处理以提高性能 - 重复预防:每次注册前调用
_listens()以防止重复 listener - 上下文比较:使用直接
===比较进行上下文匹配(仅对 parent 跟踪使用Util.stamp())
触发性能
- 提前退出:如果
listens()返回 false,则立即返回 - 最小对象创建:每个
fire()调用只创建一次 event 对象 - 数组迭代:直接对 listener 数组使用 for-of 循环(除非在触发期间修改,否则不复制)
内存优化
- 上下文为 undefined:当
context === this时,存储undefined而不是对象引用 - 延迟初始化:
_events和_eventParents等属性仅在需要时创建(??=操作符) - 基于 stamp 的 parent 键:使用数字 ID 而不是将对象存储为键