如何在委托的事件侦听器中支持 stopPropagation

How to support stopPropagation in delegated event listeners

我查看了 jQuery 的源代码,了解他们如何在委托事件侦听器中实现对 event.stopPropagation() 的支持,即 document.on(event, element...),但似乎无法让我自己的 vanilla 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...');
});

我希望事件停止在所有事件侦听器中的进一步传播,而不考虑它们的附加顺序,而不是当前行为。

我认为这只能通过在单个事件侦听器中自己执行委托事件传播来实现。在您当前的实现中,附加到 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});
}