Skip to content

事件系统 ​

目的与范围 ​

事件系统提供了发布-订阅(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(安全的可重入性)
SVG
100%

图 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 注册:

  1. 验证:检查已移除的鼠标事件并验证函数类型
  2. 重复预防:使用 _listens() 防止重复注册
  3. 上下文优化:如果 context === this,则将 context 设置为 undefined 以减少内存
  4. Listener 对象创建:创建 {fn, ctx, once?} 结构
  5. 存储:添加到 this._events[type] 数组
SVG
100%

图 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() 方法支持多种移除模式:

模式行为示例
无参数移除所有 listenerobj.off()
仅类型移除该类型的所有 listenerobj.off('click')
类型 + 函数移除特定 listenerobj.off('click', fn)
类型 + 函数 + 上下文移除带上下文的 listenerobj.off('click', fn, ctx)
Object移除多对类型/listenerobj.off({click: fn1, move: fn2})

可重入安全性: 如果在 fire() 期间移除 listener,它们会被替换为 Util.falseFn 空操作,并且数组会被复制以防止迭代器损坏。

SVG
100%

图 3:off() 方法决策树

Event 触发 ​

基本 Event 触发:fire() ​

fire() 方法使用 event 对象触发已注册的 listener:

map.fire('click', {
    latlng: L.latLng(51.5, -0.09),
    originalEvent: domEvent
});

Event 对象结构:

属性描述来源
typeEvent 类型字符串来自 fire() 参数
target触发 event 的对象this
sourceTarget原始源对象data.sourceTarget 或 this
propagatedFrom传播 event 的 parent在传播期间设置
...data自定义属性从 data 参数展开

触发流程 ​

SVG
100%

图 4:Event 触发序列

可重入保护 ​

_firingCount 属性跟踪嵌套的 fire() 调用。当 listener 修改 listener 数组时(在 fire() 期间通过 off()):

  1. 修改后的 listener 的 fn 被替换为 Util.falseFn
  2. 在 splicing 之前复制 _events[type] 数组
  3. 防止迭代器损坏并确保可预测的执行顺序

Event 传播 ​

Event Parent 关系 ​

Leaflet 使用 event parent 在组件层次结构中向上传播 event。例如,Marker 可以将其 event 传播到包含它的 FeatureGroup,后者再传播到 Map。

SVG
100%

图 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:

  1. 遍历 Object.values(this._eventParents ?? {})
  2. 对于每个 parent,调用 parent.fire(type, modifiedEvent, true)
  3. 修改后的 event 包括:
    • propagatedFrom:传播的直接子对象
    • 所有原始 event 属性
    • target 更新为 parent(在 fire() 中完成)
SVG
100%

图 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包含传播的完整检查

实现逻辑 ​

SVG
100%

图 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 搜索:

  1. 如果没有 _events 或该类型没有 listener,则返回 false
  2. 如果没有请求特定函数,则返回 true(作为 !!listeners.length)
  3. 优化上下文:如果 context === this,则将 context 设置为 undefined
  4. 使用 Array.findIndex() 定位匹配的 {fn, ctx} 对
  5. 返回索引号或 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 而不是将对象存储为键