自定义事件监听器回调为何会触发两次?

3

我创建了一个自定义元素:

const templ = document.createElement('template');
templ.innerHTML = `
<span><slot></slot></span>
`;

class SlideButton extends HTMLElement {

    constructor() {
        super();

        // Attach a shadow root to the element.
        const shadowRoot = this.attachShadow({ mode: 'open' });
        shadowRoot.appendChild(tmpl.content.cloneNode(true));
        this.span = shadowRoot.querySelector('span');

        this.triggerEvent = new CustomEvent("trigger", {
            bubbles: false,
            cancelable: false,
        });

        this.initMouseEvents();
    }

    initMouseEvents() {
        this.span.addEventListener('mousedown', (e) => {

            //Watch and calculate slide amount..
            this.addEventListener('mousemove', this.slide, false);

        });

        //When button is released...
        document.addEventListener('mouseup', handleMouseUp = (e) => {

            this.removeEventListener('mousemove', this.slide, false);
            document.removeEventListener('mouseup', handleMouseUp);

            //If slided enough, dispatch event...
            if (Math.abs(this.slideAmount) > (this.maxSlide * 0.75)) {
                console.log('firing event');
                this.dispatchEvent(this.triggerEvent);
            }
            //Reset button to normal state...

        }, false);
    }
}

在我的代码中的其他位置...

class SpotLightModal {

    //Constructor..
    //code..
    //code..

    init() {
        this.actions.querySelector('slide-button[type="den"]').addEventListener('trigger', e => {
            console.log(e);
            console.log('den');
            //Do stuff..
        });
    }

    //code...
    //code...

}

除了事件监听器中回调运行两次并输出以下内容外,一切均按预期工作:
firing event
CustomEvent {...}
den
CustomEvent {...}
den

e.stopPropagation()e.preventDefault() 都没有效果,尝试使用它们也无济于事。

我已经编辑了文本以包括 this.span,并将 "mouseup" 事件监听器移动到 "mousedown" 事件监听器之外,但这没用,实际上,现在记录 this 时,会给出另一个不同的元素(同一类型的 <slide-button>,页面上的第一个), "mouseover" 监听器没有被移除,并且事件也没有被触发。

我在这里做错了什么?或者我缺少了什么?

提前致谢。


我认为这是因为您正在将事件嵌套在事件中所导致的。 - Chris Hemmens
@ChrisHemmens 我尝试过以不同的顺序排列它们,但是没有任何作用... 其他的顺序会导致事件多次触发或根本不触发。 - Nasa
4个回答

11
如果有其他人遇到类似的问题,请尝试在回调函数中使用以下代码:event.stopImmediatePropagation()
window.addEventListener('message', (event) => {
  event.stopImmediatePropagation();
  console.log('detail:', event.detail);
})

在我的情况下,这似乎起了作用,但这绝对是超级hacky的,如果需要超级可靠,建议尝试找出问题的根本原因。

https://developer.mozilla.org/en-US/docs/Web/API/Event/stopImmediatePropagation


1
谢谢 - > 对我来说没问题!!! - H.jalali

1
在我的情况下,我使用事件总线来分发自定义事件,这将触发回调函数,在其中发生了双重事件。
最初,自定义事件侦听器位于constructor()中。
constructor() {
    EventBus.addEventListener('someEvent', this.doSomething);
}

当我把那行代码移到connectedCallback()后,重复事件就不再出现了:

connectedCallback() {
   EventBus.addEventListener('someEvent', this.doSomething);
}

0

有一个“bubbles”选项,你可以将其设置为false,这样就可以消除鼠标按下/松开的冒泡阶段:

var evt = new CustomEvent(name, {
    detail : data,
    bubbles: true, // <-- try setting to false
    cancelable: true
});

document.dispatchEvent(evt);

0

问题在于您的代码嵌套。

首先,您添加了第一个mousedown事件监听器,当鼠标被点击但未释放时将触发该事件。

this.span.addEventListener('mousedown', (e) => { ...

然后在你的mousedown事件监听器中,你监听document上的mouseup事件。

document.addEventListener('mouseup', handleMouseUp = (e) => { ...

现在,每当您单击时,mousedownmouseup都将被触发。即使您在mouseup事件侦听器内部删除事件侦听器。那里面的代码已经执行。
这就是导致您的代码触发CustomEvent两次的原因。

除非一个事件依赖于另一个事件,否则请避免嵌套事件侦听器。例如,在mousedown事件侦听器中,您需要添加mousemove事件侦听器。现在,不要嵌套,而是添加另一个事件侦听器,该事件侦听器与mousedown事件侦听器相邻,并删除mousemove事件。以下是示例:

class SlideButton extends HTMLElement {

    constructor() {
        super();

        // Attach a shadow root to the element.
        const shadowRoot = this.attachShadow({ mode: 'open' });
        shadowRoot.appendChild(tmpl.content.cloneNode(true));
        this.span = shadowRoot.querySelector('span');

        this.triggerEvent = new CustomEvent("trigger", {
            bubbles: false,
            cancelable: false,
        });

    }

    connectedCallback() {

        // It's preferred to set the event listeners in the connectedCallback method.
        // This method is called whenever the current element is connected to the document.
        this.initMouseEvents();

    }

    initMouseEvents() {

        // Bind slider-button as this context to the slide method.
        // In case you use the this keyword inside of the slide method
        // you would need to do this to keep using the methods of this class.
        const slide = this.slide.bind(this);

        // Create a toggle to indicate that the slide-button has been clicked so that ONLY then the event listeners will be added.
        let targetIsClicked = false;

        // Mouse is clicked, mousemove added.
        document.addEventListener('mousedown', (e) => {

            // Check if this button has been clicked.
            const slideButton = e.target.closest('slide-button');
            if (slideButton === this) {

                // Toggle true and add mousemove event.
                targetIsClicked = true;
                document.addEventListener('mousemove', slide, false);
            }

        });

        // Mouse is released, remove mousemove and fire event.
        document.addEventListener('mouseup', (e) => {

            // Only do something if this button has been clicked in the mousedown event.
            if (targetIsClicked === true) {
                document.removeEventListener('mousemove', slide, false);

                // Reset toggle value.
                targetIsClicked = false;

                //If slided enough, dispatch event...
                if (Math.abs(this.slideAmount) > (this.maxSlide * 0.75)) {
                    console.log('firing event');
                    this.dispatchEvent(this.triggerEvent);
                }
                //Reset button to normal state...

            }

        });

    }
}

编辑

我已将事件监听器的目标更改为document。您解释说,当在按钮外触发mousemovemouseup事件时,您希望它们能够正常工作。

mousedown中,我检查当前被点击的目标是否实际上是按钮。如果是,则会切换targetIsClicked值以指示已单击正确的按钮。

mouseup事件中,首先检查targetIsClicked是否为true。这意味着您单击了按钮并执行其余代码。

但是,如果您有多个<slide-button>元素,则会将多个mousedownmousemovemouseup监听器附加到document上,这可能会导致一些奇怪的结果。

注意

我已将this.initMouseEvents()函数调用移至connectedCallback()方法中。这样做是一个好习惯,因为现在你正在与自定义元素的生命周期进行交互。在元素创建时,在connectedCallback()中添加事件监听器,并在元素被删除时在disconnectedCallback()中删除事件监听器。元素本身触发这些方法,所以你不必调用它们。

this.span 是我的自定义元素内的一个 Shadow DOM 元素(它是应该滑动的部分)。 - Nasa
将“mouseup”事件附加到“this”不会导致它仅在鼠标释放时触发,而鼠标位于元素上吗?我以前有过这个问题,但我希望它也在鼠标在其他地方释放时起作用,这就是为什么我将侦听器添加到文档中的原因...等我回家后会尝试您的示例。 - Nasa
你能否修改你的代码,使其在你的 Shadow DOM 中包含 <span> 元素?是的,使用 this 只会在元素上触发它。所以也许 document 是更好的选择。但在修改答案之前,我会等待你的代码添加。 - Emiel Zuurbier
我已经编辑了我的答案并尝试解释了大部分内容。请查看并告诉我是否有所帮助。 - Emiel Zuurbier
不幸的是没有运气,遵循您的示例:第一次测试按钮时,它工作正常(按钮本身,滑动良好并在释放时重置),但是事件监听器仍会运行两次...第二次测试会导致错误行为,“mousemove”未被删除,事件也未被触发 :/ - Nasa
嗯,我尝试修复你的jsfiddle,但我的解决方案似乎不起作用。我已经没有更多的想法了。 - Emiel Zuurbier

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接