【问题标题】:removeEventListener not working as expectedremoveEventListener 未按预期工作
【发布时间】:2018-03-14 18:16:55
【问题描述】:

我的 Vue 应用程序中有一个相当简单的组件。当用户打开菜单时,我向#app 元素添加了一个事件监听器来检测“外部”点击。

当用户点击外部时,它会关闭菜单并移除事件监听器。这按预期工作。问题是,当用户单击按钮打开菜单,然后再次单击以关闭菜单时,我removeEventListener,因为菜单现在已关闭。

但是这不起作用。当我通过按钮打开菜单然后通过按钮将其关闭时,即使我正在删除它,事件侦听器仍停留在 #app 元素上。

如果您多次单击按钮,然后在值显示true 时单击按钮外部,您将多次看到控制台日志outside click

new Vue({
    el: "#app",
    data: {
        menuVisible: false
    },
    methods: {
        click(e) {
            var clickSource = e.target;
            var app = document.getElementById("app");
            var that = this;

            function clickOutside(e) {
                if (e.target != clickSource) {
                    console.log("outside click");
                    that.menuVisible = false;
                    app.removeEventListener("click", clickOutside);
                }
            }

            if (this.menuVisible === false) {
                this.menuVisible = true;
                app.addEventListener("click", clickOutside);
            } else if (this.menuVisible === true) {
                this.menuVisible = false;
                app.removeEventListener("click", clickOutside);
            }
        }
    }
});
body {
   margin: 0;
}

#app {
min-height: 100vh;
}
<script src="https://unpkg.com/vue"></script>

<div id="app">
  {{menuVisible}}
  <button @click="click">
  click
  </button>
</div>

【问题讨论】:

  • 在猜测中,你传递给 removeEventListener 的函数与传递给 addEventListener 的函数不同(这几乎是 removeEventListener 不起作用的唯一原因;另一个是在错误的元素,或传递了不同的第三个参数),但如果不离开现场我不能确定。
  • @T.J.Crowder 对不起,我应该知道得更多。我会更新问题。

标签: javascript events vue.js vuejs2


【解决方案1】:

问题在于,在每个click 上,您都在创建一个 clickOutside 函数;如果您将该函数与removeEventListener 一起使用,由于该exact 函数对象没有事件注册,因此不会删除任何内容。

请记住处理程序对象,以便您可以将其用于删除,请参阅注释:

new Vue({
    el: "#app",
    data: {
        menuVisible: false,
        clickOutside: null
    },
    methods: {
        click(e) {
            var clickSource = e.target;
            var app = document.getElementById("app");
            var that = this;
            
            // Only create it if we don't already have it
            if (!this.clickOutside) {
                // Create it
                this.clickOutside = function clickOutside(e) {
                    console.log("clickOutside triggered");
                    if (e.target != clickSource) {
                        console.log("It's an outside click");
                        that.menuVisible = false;
                        // Remove it
                        app.removeEventListener("click", that.clickOutside);
                    }
                };
            }

            if (this.menuVisible === false) {
                this.menuVisible = true;
                app.addEventListener("click", this.clickOutside);
            } else if (this.menuVisible === true) {
                this.menuVisible = false;
                app.removeEventListener("click", this.clickOutside);
            }
        }
    }
});
body {
   margin: 0;
}

#app {
  min-height: 100vh;
  background-color: #ddd;
}
<script src="https://unpkg.com/vue"></script>

<div id="app">
  {{menuVisible}}
  <button @click="click">
  click
  </button>
</div>

旁注:使用起来更惯用

if (this.menuVisible) {
    this.menuVisible = false;
    app.removeEventListener("click", this.clickOutside);
} else {
    this.menuVisible = true;
    app.addEventListener("click", this.clickOutside);
}

甚至

this.menuVisible = !this.menuVisible;
if (this.menuVisible) {
    app.addEventListener("click", this.clickOutside);
} else {
    app.removeEventListener("click", this.clickOutside);
}

...而不是将布尔值与truefalse=== 进行比较。你已经一个布尔值,所以...我的意思是,你在哪里停下来? if ((this.menuVisible) === true === true)? :-)

【讨论】:

  • 附注的要点。我当然有办法改善我的代码的“冗长性”:/
【解决方案2】:

这对我来说就像一个 X-Y 问题。理想情况下,您不应该在 JS(甚至 VueJS)中动态绑定/取消绑定事件监听器。如果您只是想要以下行为:

  • 按钮点击切换菜单
  • 点击按钮外的应用关闭菜单

...那么你甚至不需要凌乱的事件处理。第一个行为可以通过一个简单的真/假开关来实现,即

this.menuVisible != this.menuVisible

记得在按钮上的@click 事件绑定器上使用.stop,这样您就可以阻止VueJS 点击事件冒泡到应用程序。

同时,当在#app 但在按钮之外检测到点击事件时,只需将this.menuVisible 设置为false 即可完成第二个行为。

new Vue({
  el: "#app",
  data: {
    menuVisible: false
  },
  methods: {
    click(e) {
      console.log('button clicked');
      this.menuVisible = !this.menuVisible;
    },

    appClick(e) {
      console.log('outside clicked');
      this.menuVisible = false;
    }
  }
});
body {
  margin: 0;
}

#app {
  min-height: 100vh;
}
<script src="https://unpkg.com/vue"></script>

<div id="app" @click="appClick">
  {{menuVisible}}
  <button @click.stop="click">
  click
  </button>
</div>

【讨论】:

  • 这对我的例子来说是一个完美的解决方案。但是,我创建了问题的简化版本;实际上,此功能存在于单个文件 Vue 组件中,可在整个应用程序中重复使用。
  • @justinw 嗯,我看不出单个文件的 Vue 组件会有什么问题。需要详细说明吗?
  • 老实说我不完全确定...在应用程序中,#app 存在于我的布局中(default.vue - 我正在使用 nuxt)。此问题中引用的菜单组件在整个应用程序中多次使用,并且总是被其他组件使用。例如appHeader.vue 导入我的菜单组件。所以我想,如果我在default.vue 布局中创建appClick 方法,我需要将其作为道具传递给appHeader,然后将菜单组件作为道具并为所有使用的组件重做菜单组件。同样,我是 Vue 的新手,所以我可能/可能是错的。
  • 如果 menuVisible 是一个全局应用程序状态,并且您不想将其作为道具传递给所有菜单组件,那么您将不得不考虑使用 VueX 进行状态管理。但是是的,如果您不想使用 VueX 跟踪全局状态,则必须将 menuVisible 传递给所有菜单道具(这可能很麻烦,尤其是如果您的菜单组件嵌套了多个组件级别)。
  • 是的,这是我的基本思考过程,很高兴得到更有经验的人的证实。
猜你喜欢
  • 2021-06-04
  • 2022-01-24
  • 2015-05-11
  • 2020-05-15
  • 2014-10-31
  • 2018-02-12
  • 2014-01-20
  • 2015-01-13
  • 2013-08-01
相关资源
最近更新 更多