缘起

有时候,在项目中出现的问题,往往是因为对一些基本概念理解的不太透彻,导致在使用过程中进行大量的误用,最后导致在找 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. o n 这 个 方 式 去 添 加 监 听 事 件 , 通 过 e v e n t B u s . on 这个方式去添加监听事件,通过 eventBus. oneventBus.emit 去派发事件的。

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

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

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

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

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

思考

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

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

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

Logo

前往低代码交流专区

更多推荐