Контекст выполнения обработчиков

Событийная модель является одной из ключевых частей библиотеки Leaflet. Практически любое взаимодействие с картой сопровождается возникновением событий: клики, перемещение карты, изменение масштаба, загрузка слоёв, открытие всплывающих окон и многие другие действия. Для обработки таких событий используются функции-обработчики.

При работе с обработчиками важно понимать, в каком контексте выполняется функция, что содержит объект this, каким образом передаются данные события и какие особенности возникают при использовании различных способов объявления функций.


Объект события

Большинство обработчиков Leaflet получают специальный объект события в качестве первого аргумента.

Пример:

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

При клике по карте в консоль выводится объект события, содержащий множество полезных данных:

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

Результат:

{
    lat: 55.751244,
    lng: 37.618423
}

Через объект события можно получать:

  • координаты точки клика;
  • координаты курсора;
  • ссылку на исходное DOM-событие;
  • объект, вызвавший событие;
  • дополнительную служебную информацию.

Тип содержимого зависит от конкретного события.


Значение this внутри обработчика

Одной из особенностей системы событий Leaflet является автоматическая установка контекста выполнения.

Рассмотрим обработчик маркера:

marker.on('click', function() {
    console.log(this);
});

После клика будет выведен объект маркера:

L.Marker

Это означает, что внутри функции this указывает на объект, который сгенерировал событие.

Эквивалентно:

marker.on('click', function() {
    this.openPopup();
});

Здесь метод openPopup() вызывается непосредственно у маркера, потому что this содержит ссылку на текущий объект.


Контекст для различных объектов

Leaflet сохраняет единый подход для всех классов.

Карта

map.on('zoomend', function() {
    console.log(this === map);
});

Результат:

true

Маркер

marker.on('click', function() {
    console.log(this === marker);
});

Результат:

true

Полигон

polygon.on('mouseover', function() {
    console.log(this === polygon);
});

Результат:

true

Полилиния

polyline.on('click', function() {
    console.log(this === polyline);
});

Результат:

true

Такое поведение значительно упрощает работу с объектами внутри обработчиков.


Использование объекта события вместо this

Хотя доступ через this удобен, существует альтернативный способ получения объекта-источника.

marker.on('click', function(event) {
    console.log(event.target);
});

Свойство:

event.target

содержит ссылку на объект, который вызвал событие.

Пример:

marker.on('click', function(event) {
    event.target.openPopup();
});

Этот вариант считается более явным и часто используется в крупных проектах.


Сравнение this и event.target

Оба подхода дают доступ к одному объекту.

marker.on('click', function(event) {
    console.log(this === event.target);
});

Результат:

true

Поэтому записи являются эквивалентными:

this.setOpacity(0.5);

и

event.target.setOpacity(0.5);

Выбор обычно зависит от принятого стиля кодирования.


Потеря контекста

Одна из самых распространённых ошибок связана с передачей метода объекта напрямую в обработчик.

Допустим, существует объект:

const app = {
    name: 'Map Application',

    showInfo() {
        console.log(this.name);
    }
};

Попытка зарегистрировать метод напрямую:

marker.on('click', app.showInfo);

может привести к неожиданному результату.

Во время выполнения Leaflet изменяет контекст вызова, поэтому внутри метода:

this

будет содержать объект маркера, а не объект app.

Соответственно:

console.log(this.name);

выведет:

undefined

или другое значение.


Привязка контекста через bind

Для сохранения нужного контекста используется метод bind().

marker.on('click', app.showInfo.bind(app));

Теперь:

this === app

и вывод будет корректным:

Map Application

Метод создаёт новую функцию, жёстко связанную с указанным объектом.


Передача контекста через третий аргумент on

Leaflet предоставляет встроенный механизм задания контекста.

Синтаксис:

object.on(type, fn, context);

Пример:

marker.on(
    'click',
    app.showInfo,
    app
);

Внутри метода:

this === app

Такой подход часто оказывается удобнее, чем использование bind().


Пример с пользовательским объектом

const controller = {
    selected: 0,

    handleClick() {
        this.selected++;
        console.log(this.selected);
    }
};

marker.on(
    'click',
    controller.handleClick,
    controller
);

После каждого клика:

1
2
3
4
...

Состояние хранится внутри объекта контроллера, а не внутри маркера.


Особенности стрелочных функций

Стрелочные функции работают иначе.

Рассмотрим пример:

marker.on('click', () => {
    console.log(this);
});

Здесь значение this не устанавливается Leaflet.

Стрелочная функция наследует контекст из внешней области видимости.

Например:

class App {
    constructor(map) {
        this.map = map;

        marker.on('click', () => {
            console.log(this.map);
        });
    }
}

Внутри обработчика:

this

будет ссылаться на экземпляр класса App.

Это поведение отличается от обычных функций.


Сравнение обычной и стрелочной функции

Обычная функция:

marker.on('click', function() {
    console.log(this);
});

Вывод:

L.Marker

Стрелочная функция:

marker.on('click', () => {
    console.log(this);
});

Вывод:

Window

или другой внешний объект в зависимости от окружения выполнения.

Поэтому при работе с Leaflet важно осознанно выбирать тип функции.


Когда использовать обычные функции

Обычная функция подходит, если нужен доступ к объекту, вызвавшему событие.

Например:

marker.on('click', function() {
    this.setOpacity(0.4);
});

или

polygon.on('mouseover', function() {
    this.setStyle({
        color: 'red'
    });
});

В таких случаях встроенный контекст Leaflet оказывается очень удобным.


Когда использовать стрелочные функции

Стрелочные функции полезны, если требуется сохранить контекст внешнего объекта.

Например:

class MapManager {

    constructor() {
        this.counter = 0;

        marker.on('click', () => {
            this.counter++;
        });
    }
}

Здесь обработчик работает с экземпляром класса, а не с маркером.


Доступ к объекту карты из обработчика слоя

Многие объекты Leaflet содержат ссылку на карту.

Пример:

marker.on('click', function() {
    console.log(this._map);
});

Однако использование внутренних свойств считается нежелательным.

Лучше заранее сохранить ссылку:

const mapRef = map;

marker.on('click', function() {
    mapRef.setZoom(12);
});

или использовать замыкание.


Замыкания и контекст

Функции-обработчики имеют доступ к переменным внешней области видимости.

const cityName = 'Москва';

marker.on('click', function() {
    console.log(cityName);
});

После клика:

Москва

Это возможно благодаря механизму замыканий JavaScript.


Контекст внутри нескольких обработчиков

Один объект может иметь несколько обработчиков одного события.

marker.on('click', function() {
    console.log('Первый');
});

marker.on('click', function() {
    console.log('Второй');
});

Каждый обработчик получает собственный контекст выполнения.

Во всех случаях:

this === marker

Удаление обработчиков и контекст

Для корректного удаления необходимо использовать ту же функцию, которая была зарегистрирована ранее.

function onMarkerClick() {
    console.log('Click');
}

marker.on('click', onMarkerClick);

Удаление:

marker.off('click', onMarkerClick);

Если используется bind(), следует сохранить ссылку:

const boundHandler =
    app.showInfo.bind(app);

marker.on('click', boundHandler);

marker.off('click', boundHandler);

Иначе удалить обработчик не получится, поскольку каждый вызов bind() создаёт новый объект функции.


Практический пример управления маркерами

const manager = {

    selectedMarker: null,

    selectMarker(event) {

        if (this.selectedMarker) {
            this.selectedMarker.setOpacity(1);
        }

        this.selectedMarker = event.target;

        this.selectedMarker.setOpacity(0.5);
    }
};

marker1.on(
    'click',
    manager.selectMarker,
    manager
);

marker2.on(
    'click',
    manager.selectMarker,
    manager
);

marker3.on(
    'click',
    manager.selectMarker,
    manager
);

Здесь:

  • this внутри обработчика указывает на объект manager;
  • выбранный маркер доступен через event.target;
  • состояние приложения хранится отдельно от объектов карты.

Подобная архитектура широко используется при создании сложных картографических интерфейсов, содержащих большое количество интерактивных элементов и пользовательской логики.