removeEventListener “不生效”的思考

缘起

有时候,在项目中出现的问题,往往是因为对一些基本概念理解的不太透彻,导致在使用过程中进行大量的误用,最后导致在找 bug 的过程中搞得心力交瘁。

最近我就碰到一起由于 removeEventListener 移除“失效” 导致引起的 bug。

起因是由于,我们之前做了一个项目,项目是用 vue 写的。由于是和同事一起写的,所以对于有些地方,掌握的还是很难达到自己写的代码那么透彻的。

因为我们的项目需要做到页面的自适应,因此我们会监听 resize 事件,然后做一些自适应的调整。

但是某天,同时突然过来反映说,由于我之前在某个根组件,用了 v-if 的属性,导致组件会被销毁,但是销毁的过程中,子组件中的监听事件没有被销毁掉,导致会出现疯狂报错的情况。

一顿操作猛如虎

作为一枚程序员,碰到问题的第一反映,当然是马上进行调试,试图找到问题所在。

于是,一顿测试,确实发现存在这个问题。

而且由于事件没有被销毁掉,如果窗口不断的在两种情况下相互切换,会导致监听事件一直堆积在内存中,无法销毁掉,极度影响项目的性能。

但是这个问题,很多时候其实是不影响项目的正常使用的,毕竟,正常使用的时候,没有人会一直闲的无聊,去改变可可是区域窗口的大小的吧,

调试页面的性能问题,也是有技巧的,有人说打断点,或者用 console 啥的。

这些方式我都尝试过,不过最近发现一个更直观的工具,那就是 chrome devtool 提供的 preformance monitor 功能,这个可以实时的统计出来页面的内存占用,页面上已经添加的监听事件数量等等情况。

image.png

没用过的朋友们,可以打开 chrome devtool 尝试下。

细微之处见真知

但是本着打破砂锅问到底的精神,我还是研究了一下为什么会出现这个问题。

碰到这个问题,我的第一反应是,是不是自己在写代码的过程中,由于疏忽,没有在组件销毁之前移除掉添加的监听事件呢?

但是我通读了一遍代码以后,发现每次 beforeDestroy 的时候,我都有调用了 removeEventListener 去移除事件。

后面我又想,难道是,v-if 没有在条件不成立的时候,销毁掉组件么?但是我马上又否认了这个想法,如果 vue 有这么大的 bug,我应该没有机会成为它的第一个发现者吧,毕竟 vue 是一个如此成熟、火爆的前端框架。

但是为了保险起见,我还是用代码调试了一下,结果发现,并没有问题,组件确实是被销毁掉了。

但是为什么事件没有被销毁掉呢?

难道是因为我移除事件的方法有问题?

于是不甘认输的我写了个页面进行测试了一下,还好,我以往的认知并没有这么快被打破,但是问题究竟出现在哪儿呢?

为了模拟 vue 组件销毁和添加的时候,事件添加和移除的逻辑,我写了如下一个测试代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>resize test</title>
</head>
<body>
  <button id="add" onclick="add()">添加事件</button>
  <button id="remove" onclick="remove()">移除事件</button>
  <script>
    let eventList = {
      0: () => {
        console.log(0);
      },
      1: () => {
        console.log(1);
      }
    };
    let count = 0;
    function add(e){
      window.addEventListener('resize', eventList[count]);
      count += 1;
    }

    function remove(e){
      count--;
      window.removeEventListener('resize', eventList[count]);
    } 
  </script>
</body>
</html>

逻辑很简单,就是在页面上加了两个按钮,一个用来添加事件,一个用来移除事件,为了保险起见,我还添加了多个事件进行测试。

但是令人遗憾的是,测试的结果没有丝毫问题,可以正常的添加也可以正常的移除掉,这让我不禁产生了怀疑,到底是哪里出现了问题。

后来我灵机一动,莫不是移除的事件和添加的事件不是同一个,导致在项目中移除不成功,而出现问题么?

后来我再一检查项目,果然就发现问题所在了,原来是同事在优化代码的时候,把事件处理这块,也一并优化掉了。

原本我们添加到 vue 组件的 methods 中的代码,并不需要是手动绑定作用域,但是同事对这块的掌握程度不够,对监听事件手动进行了 bind 操作。

想来也是惭愧,我之前检查的时候,居然也没有发现存在这个问题,所以才导致后面折腾了这么久,才定位到问题所在。

不够其实也是自己在这种问题上认识不深导致的。

虽然从写 js 第一天开始,就知道自己手动添加的事件,必须要配套手动删除的逻辑。

不然,很多时候的小疏忽,会酿成大问题。

但是在真正写代码的时候,其实是很难从头到尾贯彻执行下去的。

了解到问题以后,我开始反思自己这种惰性思维。

我开始手动排查整个项目中的代码,发现有的地方自己也没有重视起来,也没有写上移除事件的逻辑!

不觉,大感惭愧。

追根溯源

而后我从 vue 官网找到的文档中,找到了下面一段文字:

image.png
可以看到的是,文档中很清楚的写到了,methods 中的方法的 this 会自动绑定为 Vue 实例。

至于为什么会这样,其实很好理解。

因为 Vue 的组件,其实更像是一段配置文件,一般情况下我们是无法更改配置文件的构造函数的,但是我们这地方写的方法,必须要获取到实例上的属性和方法,不然,接下来一切都无法进行了。

所以我们在添加自己的监听事件的时候,并不需要再次的 bind this,这样会导致重新生成一个方法,所以在移除的时候,肯定是移除不掉了。

但是用过 react 的人肯定会很奇怪,为什么 react 中,我必须要 constructor 中手动 bind this 呢?那是因为 vue 中自动为我们做了这样的操作,而 react 中,我们自己用类或者函数式的方式构造组件,我们必须要自己去执行这样的绑定操作。

举一反三

既然这个默认的监听会存在组件销毁未被移除的问题,那我在 vue 中用的 eventBus 会不会也出现这种问题呢?

我们在写代码的时候,特别是在写 vue 的时候,会单独构造一个事件总线(eventBus),去管理组件之间事件的传递。

一般是通过 eventBus.oneventBus.on 这个方式去添加监听事件,通过 eventBus.emit 去派发事件的。

但是,我却好像一直忘记写把这个事件总线上的监听给移除掉的逻辑了。

在项目里一检查,果然发现存在这个问题。

而 eventBus 这个事件调试起来就更简单了。eventBus 是一个 js 对象而已,而添加的事件肯定是存储在这个对象上的。

果然在 eventBus 对象上,存在 _events 这个属性,这里面就存储着我们添加的各种事件。

组件销毁的时候,还需要通过 $off 的方式,将监听事件给移除掉,不然,这个事件一直会响应。

思考

我不禁在思考,很多人说,js 不是一门很好的语言,坑太多了,这点其实我是赞成的,但是有时候这些所谓的坑,只是我们对语言的理解程度不够深造成的吧。

有时候,自由度高其实并不是缺点,最关键点在于使用者吧,

就像同样一支笔,别人写出来的字能称之为书法,而我们很多人写的,顶多只能算作是字符而已。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章