【问题标题】:Remove window event listener on unbind Vue directive lifecycle取消绑定 Vue 指令生命周期中的窗口事件侦听器
【发布时间】:2020-02-10 12:34:13
【问题描述】:

我刚刚遇到了一个与 Vue 指令中的事件监听相关的问题。 我有一个包含以下代码的组件:

function setHeaderWrapperHeight() { ... }
function scrollEventHandler() { ... }

export default {
  ...
  directives: {
    fox: {
      inserted(el, binding, vnode) {
        setHeaderWrapperHeight(el);
        el.classList.add('header__unfixed');
        window.addEventListener(
          'scroll',
          scrollEventListener.bind(null, el, binding.arg)
        );
        window.addEventListener(
          'resize',
          setHeaderWrapperHeight.bind(null, el)
        );
      },
      unbind(el, binding) {
        console.log('Unbound');
        window.removeEventListener('scroll', scrollEventListener);
        window.removeEventListener('resize', setHeaderWrapperHeight);
      }
    }
  }
  ...
}

每次更改路由器路径时都会重新渲染此组件,我通过将当前路由路径分配给:key prop 来实现此行为,因此每当路径更改时,它都会重新渲染。但是问题是尽管事件侦听器没有被删除/销毁,导致可怕的性能问题。那么如何移除事件监听器呢?

【问题讨论】:

  • 尝试在组件的 destroyed 生命周期钩子中删除事件监听器,而不是从指令中删除,
  • @DigitalDrifter 仍然得到相同的结果
  • @DigitalDrifter 是的,我正在使用 VueRouter,因为已经发现问题更多的是 javascript,因此在函数上调用 bind() 创建了新函数,因此 removeEventListener 得到了错误的函数引用。谢谢!

标签: javascript vue.js vue-directives


【解决方案1】:

在函数上调用bind 会创建一个新函数。侦听器没有被删除,因为您传递给 removeEventListener 的函数与您传递给 addEventListener 的函数不同。

在指令中的钩子之间进行通信并不是特别容易。官方文档建议使用元素的dataset,尽管在这种情况下看起来很笨拙:

https://vuejs.org/v2/guide/custom-directive.html#Directive-Hook-Arguments

您可以将侦听器直接作为属性存储在元素上,以便它们在 unbind 挂钩中可用。

下面的代码采用了稍微不同的方法。它使用一个数组来保存当前绑定到指令的所有元素。 window 上的监听器只注册一次,无论该指令使用多少次。如果当前未使用该指令,则删除该侦听器:

let foxElements = []

function onClick () {
  console.log('click triggered')

  for (const entry of foxElements) {
    clickHandler(entry.el, entry.arg)
  }
}

function clickHandler (el, arg) {
  console.log('clicked', el, arg)
}

new Vue({
  el: '#app',
  
  data () {
    return {
      items: [0]
    }
  },

  directives: {
    fox: {
      inserted (el, binding) {
        console.log('inserted')
        
        if (foxElements.length === 0) {
          console.log('adding window listener')
          window.addEventListener('click', onClick)
        }

        foxElements.push({
          el,
          arg: binding.arg
        })
      },

      unbind (el, binding) {
        console.log('unbind')
      
        foxElements = foxElements.filter(element => element.el !== el)
        
        if (foxElements.length === 0) {
          console.log('removing window listener')
          window.removeEventListener('click', onClick)
        }
      }
    }
  }
})
<script src="https://unpkg.com/vue@2.6.11/dist/vue.js"></script>

<div id="app">
  <button @click="items.push(Math.floor(Math.random() * 1000))">Add</button>
  <hr>
  <button
    v-for="(item, index) in items"
    v-fox:example
    @click="items.splice(index, 1)"
  >Remove {{ item }}</button>
</div>

但是,所有这些都假设指令甚至是正确的方法。如果您可以在组件级别执行此操作,那么它可能会变得更简单,因为您拥有可用于存储内容的组件实例。请记住,调用 bind 会创建一个新函数,因此您需要在某处保留对该函数的引用,以便将其传递给 removeEventListener

【讨论】:

    【解决方案2】:

    只是为了记录,并帮助任何通过这里的人,因为已经有一个可接受的答案,在这种情况下(至少在 Vue 3 上,未在 Vue 2 上测试)可以做的是使用binding.dir(这是对指令自身对象的引用)来承载用于在指令对象上添加事件侦听器的函数,并在以后需要删除此侦听器时将其取回。

    绑定一个焦点事件的简单示例(与原问题无关):

    export default {
      ...
      directives: {
        fox: {
          handleFocus: () => { /* a placeholder to rewrite later */ },
          mounted(el, binding) {
            binding.dir.handleFocus = () => { /* do whatever */ }
            el.addEventListener('focus', binding.dir.handleFocus);
          },
          beforeUnmount(el, binding) {
            el.removeEventListener('focus', binding.dir.handleFocus);
          }
        }
      }
      ...
    }
    

    在我的情况下,我正在做的一个实际示例是为任何输入或文本区域标签设置焦点/模糊通知程序。我为此做了一个 Gist here,它是基于 Vue 3 和 TypeScript 构建的一个项目。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-09-28
      • 2011-10-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-04-09
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多