Как поддерживать stopPropagation в делегированных прослушивателях событий

Я просмотрел исходный код из jQuery, чтобы узнать, как они реализовали поддержку event.stopPropagation() в делегированных прослушивателях событий, то есть document.on(event, element...), но, похоже, не могу заставить мою собственную ванильную реализацию JS полностью работать.

Я попытался переопределить собственный event.stopPropagation() в своем методе, чтобы просто установить event.propagationStopped для самого события и искать это при принятии решения о том, должно ли событие распространяться на своих родителей.

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

function delegate(type, selector, callback) {
    document.addEventListener(type, function(event) {
        event.stopPropagation = function() {
            event.propagationStopped = true;
        };

        var element = event.target, found;

        while (element && element.parentNode) {
            if (element.matches(selector)) {
                callback.call(element, event);
            }

            if (!event.propagationStopped) {
                element = element.parentNode;
            } else {
                break;
            }
        }
    });
}


delegate('click', '.overlay', function() {
    console.log('Close overlay');
});

delegate('click', '.modal', function(e) {
    e.stopPropagation();

    console.log('Clicked inside modal, stopping propagation...');
});

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


person André Ekeberg    schedule 28.07.2019    source источник
comment
IIRC, jQuery подключает только один фактический прослушиватель событий DOM и сохраняет обратные вызовы в свойствах данных элементов. Затем, когда событие DOM срабатывает, оно выполняет все распространение и последовательный вызов самостоятельно.   -  person Bergi    schedule 29.07.2019
comment
@Bergi Я тоже об этом думал, но надеюсь, что есть более простой способ сделать это, так как я не уверен, как их хранить (не хочу ничего прикреплять к реальным элементам), и как решить в в какой последовательности их следует называть.   -  person André Ekeberg    schedule 29.07.2019
comment
Я напишу ответ, демонстрирующий, как это сделать. Не уверен, есть ли лучший способ, мне тоже любопытно...   -  person Bergi    schedule 29.07.2019


Ответы (1)


Я думаю, что это возможно только путем самостоятельного распространения делегированного события в одном прослушивателе событий. В вашей текущей реализации второй прослушиватель, прикрепленный к document, всегда срабатывает вторым, независимо от его селектора, но когда он выбирает потомка другого селектора, вам нужно, чтобы он срабатывал первым.

Здесь я собираю список делегированных слушателей событий, а затем итерирую его для каждого элемента в обходе вашего родителя. Вы можете сохранить список в самом элементе (например, с символами или WeakMap), но, поскольку вы делегируете только document, я просто буду хранить их глобально.

const delegatedListeners = {};

function delegate(type, selector, callback) {
    if (!delegatedListeners[type]) {
        const listeners = delegatedListeners[type] = [];
        document.addEventListener(type, function(event) {
            for (let element = event.target; element && element.parentNode; element = element.parentNode) {
                // unfortunately, event.currentTarget cannot be overwritten with element
                for (const {selector, callback} of listeners) {
                    if (element.matches(selector)) {
                        callback.call(element, event);
                    }
                }
                if (event.cancelBubble) { // a getter for the propagation stopped flag :-)
                    break;
                }
            }
        });
    }
    delegatedListeners[type].push({selector, callback});
}
person Bergi    schedule 28.07.2019
comment
Я понятия не имел, что это можно решить с помощью всего лишь такого количества кода, и что мне даже не нужно было реализовывать stopPropagation, если я просто проверял родное свойство cancelBubble! Я вижу, вы добавили аргумент element, который, я считаю, был ошибкой, потому что, когда я удалил его, код начал работать (также вы забыли открывающую фигурную скобку в своем цикле for ... of). Это работает очень здорово, большое спасибо. Я также решил добавить объект слушателей как подсвойство самого delegate, поэтому я могу экспортировать только delegate как модуль es6. см. JSFiddle - person André Ekeberg; 29.07.2019
comment
@Tidsoptimisten Не стесняйтесь предлагать редактирование для таких очевидных ошибок, спасибо (сначала я подумал о более сложном решении, устанавливающем прослушиватели на произвольном element, прежде чем я понял, что вы использовали только document). Кстати, вы все еще можете export function delegate не делать слушателей свойством, сохраняя переменную const delegatedListeners скрытой в области модуля. - person Bergi; 29.07.2019
comment
О, я и не знала, что так можно, спасибо! И да, я понимаю, что можно ограничиться определенными элементами, возможно, я подумаю об этом позже. И, наконец, это круто, я не понимал ни того, что я могу экспортировать такие функции, ни того, что экспортируемая функция, очевидно, все еще имеет доступ к этим константам. - person André Ekeberg; 29.07.2019