Skip to content

Events System

Purpose and Scope

The Events System provides the publish-subscribe (pub-sub) event architecture that powers all user interactions and component communication in Leaflet. This document covers the Evented base class, event registration (on, once, off), event firing and propagation, listener management, and event parent relationships.

For information about DOM event handling and browser event normalization, see DOM Utilities and Event Handling. For the overall class inheritance system, see Utilities and Class System.

Overview

The Evented class extends Class and provides event-powered functionality to components like Map, Marker, and Layer. The system supports:

  • Multiple listeners per event type
  • Custom execution contexts
  • Space-separated event types (e.g., 'click dblclick')
  • Object-based event registration (e.g., {click: fn1, move: fn2})
  • One-time listeners via once()
  • Event propagation through parent objects
  • Listener removal during event firing (safe reentrancy)
SVG
100%

Figure 1: Evented Class Architecture

Event Registration

Adding Listeners with on()

The on() method registers event listeners with flexible syntax:

Single event type with function:

map.on('click', function(e) {
    console.log(e.latlng);
});

Multiple space-separated types:

map.on('click dblclick', handler);

Object-based registration:

map.on({
    click: onClickHandler,
    move: onMoveHandler
});

With custom context:

map.on('click', this.handleClick, this);

The implementation processes input and delegates to _on():

Input TypeProcessingExample
ObjectIterates entries, calls _on() for each{click: fn1, move: fn2}
StringSplits on whitespace, calls _on() for each'click dblclick'
Single stringDirect call to _on()'click'

Core Registration: _on()

The _on() method performs the actual listener registration:

  1. Validation: Checks for removed mouse events and validates function type
  2. Duplicate Prevention: Uses _listens() to prevent duplicate registration
  3. Context Optimization: Sets context = undefined if context === this to reduce memory
  4. Listener Object Creation: Creates {fn, ctx, once?} structure
  5. Storage: Adds to this._events[type] array
SVG
100%

Figure 2: Listener Registration Flow

One-Time Listeners: once()

The once() method registers listeners that auto-remove after first execution:

map.once('load', function() {
    console.log('Map loaded');
});

Implementation delegates to _on() with _once = true flag. During fire(), listeners with once: true are removed via this.off() before invocation.

Removing Listeners: off()

The off() method supports multiple removal patterns:

PatternBehaviorExample
No argumentsRemove all listenersobj.off()
Type onlyRemove all listeners for typeobj.off('click')
Type + functionRemove specific listenerobj.off('click', fn)
Type + function + contextRemove listener with contextobj.off('click', fn, ctx)
ObjectRemove multiple type/listener pairsobj.off({click: fn1, move: fn2})

Reentrancy Safety: If listeners are removed during fire(), they are replaced with Util.falseFn noops and the array is copied to prevent iterator corruption.

SVG
100%

Figure 3: off() Method Decision Tree

Event Firing

Basic Event Firing: fire()

The fire() method triggers registered listeners with an event object:

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

Event Object Structure:

PropertyDescriptionSource
typeEvent type stringFrom fire() argument
targetObject that fired eventthis
sourceTargetOriginal source objectdata.sourceTarget or this
propagatedFromParent that propagated eventSet during propagation
...dataCustom propertiesSpread from data argument

Firing Process

SVG
100%

Figure 4: Event Firing Sequence

Reentrancy Protection

The _firingCount property tracks nested fire() calls. When listeners modify the listener array (via off() during fire()):

  1. Modified listeners have fn replaced with Util.falseFn
  2. The _events[type] array is copied before splicing
  3. Prevents iterator corruption and ensures predictable execution order

Event Propagation

Event Parent Relationships

Leaflet uses event parents to propagate events up component hierarchies. For example, a Marker can propagate events to its containing FeatureGroup, which propagates to the Map.

SVG
100%

Figure 5: Event Propagation Hierarchy

Managing Event Parents

Adding a parent:

marker.addEventParent(featureGroup);

Removing a parent:

marker.removeEventParent(featureGroup);

Parents are stored in _eventParents using Util.stamp() for unique identification:

this._eventParents[Util.stamp(obj)] = obj;

Propagation Implementation

The _propagateEvent() method fires the event on all parents:

  1. Iterates Object.values(this._eventParents ?? {})
  2. For each parent, calls parent.fire(type, modifiedEvent, true)
  3. Modified event includes:
    • propagatedFrom: The immediate child that propagated
    • All original event properties
    • target updates to parent (done in fire())
SVG
100%

Figure 6: Event Object Transformation During Propagation

Querying Listeners: listens()

The listens() method checks for listener existence with optional propagation:

Basic usage:

if (map.listens('click')) {
    // Has click listeners
}

Check specific function:

if (map.listens('click', myHandler)) {
    // myHandler is registered for click
}

Check with propagation:

if (marker.listens('click', true)) {
    // Marker or its parents have click listeners
}

Method Signatures:

SignatureReturnsDescription
listens(type)booleanHas any listeners for type
listens(type, fn, context)booleanHas specific listener
listens(type, propagate)booleanHas listeners (checking parents if true)
listens(type, fn, context, propagate)booleanFull check with propagation

Implementation Logic

SVG
100%

Figure 7: listens() Implementation Flow

Internal Architecture

Data Structures

The Evented class maintains three primary internal properties:

_events Object:

_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 Object:

_eventParents = {
    123: featureGroupInstance,  // Key is Util.stamp(obj)
    456: mapInstance
}

_firingCount Number: Tracks nested fire() calls to handle reentrancy. Incremented on entry, decremented on exit.

Listener Lookup: _listens()

The _listens() method performs the core listener search:

  1. Returns false if no _events or no listeners for type
  2. Returns true (as !!listeners.length) if no specific function requested
  3. Optimizes context: sets context = undefined if context === this
  4. Uses Array.findIndex() to locate matching {fn, ctx} pair
  5. Returns index number or false

This return value (number vs. false) is used by _off() to splice the array.

Context Optimization

To reduce memory footprint, Leaflet treats context === this as context = undefined:

if (context === this) {
    context = undefined;
}

This optimization appears in both _on() and _listens(), ensuring consistent matching during registration and lookup.

Common Usage Patterns

Map Event Handling

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 Bubbling

Layers automatically set the map as an event parent, enabling propagation:

const marker = L.marker([51.5, -0.09]).addTo(map);

// Listen on map for marker events
map.on('click', function(e) {
    if (e.sourceTarget === marker) {
        console.log('Marker clicked');
    }
});

// Fire propagates: marker → map
marker.fire('click', {}, true);

Memory Management

Remove listeners to prevent memory leaks:

function onClick(e) { /* ... */ }

map.on('click', onClick);

// Later, when done:
map.off('click', onClick);

// Or remove all listeners:
map.off();

Temporary Listeners

Use once() for one-time events:

map.once('load', function() {
    console.log('Initial load complete');
});

// Or remove within handler:
map.on('click', function handler(e) {
    console.log('First click');
    map.off('click', handler);
});

Integration with Class System

Evented extends Class, inheriting the option system and initialization hooks. Classes that need event functionality simply extend Evented:

import {Evented} from './core/Events.js';

class MyComponent extends Evented {
    initialize(options) {
        // Component initialization
    }
    
    doSomething() {
        this.fire('something', {data: 'value'});
    }
}

const component = new MyComponent();
component.on('something', (e) => {
    console.log(e.data);
});
component.doSomething();  // Fires event

Performance Considerations

Event Registration Performance

  • Object syntax optimization: When using on({type1: fn1, type2: fn2}), space-separated type processing is skipped for performance
  • Duplicate prevention: _listens() is called before every registration to prevent duplicate listeners
  • Context comparison: Direct === comparison for context matching (uses Util.stamp() only for parent tracking)

Firing Performance

  • Early exit: Returns immediately if listens() returns false
  • Minimal object creation: Event object created once per fire() call
  • Array iteration: Direct for-of loop over listeners array (no copying unless modified during firing)

Memory Optimization

  • Context as undefined: When context === this, stores undefined instead of object reference
  • Lazy initialization: Properties like _events and _eventParents are created only when needed (??= operator)
  • Stamp-based parent keys: Uses numeric IDs instead of storing objects as keys