Событийная модель является одной из ключевых частей библиотеки 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
}
Через объект события можно получать:
Тип содержимого зависит от конкретного события.
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().
marker.on('click', app.showInfo.bind(app));
Теперь:
this === app
и вывод будет корректным:
Map Application
Метод создаёт новую функцию, жёстко связанную с указанным объектом.
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;Подобная архитектура широко используется при создании сложных картографических интерфейсов, содержащих большое количество интерактивных элементов и пользовательской логики.