你说什么?定时器setInterval用clearInterval 清不掉,怎么可能?

814 阅读5分钟

距离上一次的文章《了解策略模式(Strategy Pattern)的应用就从需求开始吧》过去过去了整整20多天了,我又来了,今天我们来看一个问题,同事他说他设置的定时器setInterval用clearInterval 清不掉,怎么可能啊?调用了clearInterval 定时器还在跑;

背景提要

那天就在快中午吃饭的时候,同事发消息说他开启的定时器 setInterval 在页面离开的时候还在,说 beforeRouterLeave 这个理由钩子不能用了吗?

我满脸问号? 怎么回事? 怎么可能? 绝对不可能?

按照常理怎么可能呢?因为到午餐时间了,就说下午睡醒后再看吧!

时间来到了下午去到他的工位,让给讲一下具体的原因,因为从上午的表述 确实听不出来什么问题。

经过简单交流了解到具体需求,即:在这个页面会有一个自动打印的功能,受几个系统参数的控制,离开页面需要把自动打印停了;本打算使用路由钩子 beforeRouterLeave 去清除定时器;但是退出当前页面后依然没有停止,还在继续。这就很迷惑.....

这一听听上去很简单啊,那为啥还会清不掉呢;

场景复现

因为是内网项目,代码不能上外网也不允许直接贴,我就简单的模拟了一下当时的代码如下:

<template>
  <div class="about">
    <h1>This is an demo page </h1>
    <button @click="handleStop">停止打印</button>
  </div>
</template>

<style>
@media (min-width: 1024px) {
  .about {
    min-height: 100vh;
    display: flex;
    align-items: center;
  }
}
</style>
<script>
export default {
  data(){
    return {
      timer:null,
      temp1:1,
      temp2:2
    }
  },
  mounted(){
    setTimeout(()=>{
      this.temp1 +=1
    },1000)
    setTimeout(()=>{
      this.temp2 +=2
    },2000)
    this.$watch(()=>{
      return this.temp1 + this.temp2
    },(newVal,oldVal)=>{
      this.handleAutoPrint()
    })
  },
  methods:{
    handleStop(){
      if(this.timer){
        clearInterval(this.timer)
        this.tiemr = null
        console.log("停止自动打印")
      }
    },
    task(){
        console.log("开始自动打印....")
    },
    handleAutoPrint(){
      this.timer = setInterval(()=>{
        this.task()
      },3000)
    }
  },
  beforeDestroy(){
  }
}
</script>

经过简单的排查居然没发现问题

但是通过运行的结果一看,即使我们点亮了“停止”按钮定时器依然不会停止; 那么原因何在呢?

###分析与解决 那既然定时器没有在我们点击 “停止”按钮后 结束,那指定是定时器没有被有效的清除啊,还是在控制台持续打印“开始自动打印....” 那么我们基本就认定事实定时器没有被销毁 那是 clearInterval 这个API 不起效果了吗,当然不是 官方API失效这个概率为0, 那只能是代码本身的原因了

那既然我们 调用了 clearInterval 同时也将timer 设置为null 了,还在执行定时器内的任务。那此时我们可以在执行任务的地方加上判断条件如下: 于是就有了下面的代码:

task(){
  // 我们加上判断条件  
  if(this.timer){
    console.log("开始自动打印....")
  }
},

我们加上了判断条件,果然 当我们点击了 停止 按钮的时候,日志不再输出"开始自动打印...."

那么到这里我们的目标就达到了吗?这样修改会不会有什么问题

那通常这么问了,答案肯定是否定的;

那我们继续分析:

既然定时器都清除了并且重新赋值为null 了,那为什么还在执行呢

经过我们认真的审阅代码发现,那一定是存在多个定时器了,定时器没有清除干净;可是我们明明使用的

clearInterval(this.timer);this.tiemr = null

这代码 肯定是没问题的;

答案就是 他创建了多个定时器,我们通过观察发现 在wathc 中 监听了两个变量,然后执行了 开启定时器 执行task 的函数

我们知道 watch 只要监听的目标值发生了变化,那么就会执行回调函数;

那么问题就被成功定位了

就是 两个变量的两次变化都触发了 开启定时器,并把定时器赋值给 timer 变量;

那么 timer 最终指向的就是最后一次 watch 回调函数的 定时器,

那之前的开启的定时器呢? 不会被系统回收吗?

当然不会了,我们在日常的性能优化中都知道有一点就是 定时器使用完了,要清除回收; 因为第一个定时器依旧在执行,所以不会被系统回收; 由于第二个定时器被指向了tiemr 变量,所以我们可以被清除掉;

那么就真相大白了。

我们就知道上面 在 task 函数中 直接加的判断条件 其实是有问题的, 虽然达到了我们的预期,停止了task 的执行; 但是两个定时器任务却依然在跑着,只是在每个时间周期执行的task 没有实际的业务操作,但是他们始终在运行; 这就产生了内存常驻;

那么我们甚至可以通过 浏览器开发工具 内存快照定位到该问题。

所以我们最终的解决方案是,在开启定时器之前 我们先执行清除定时器;确保这个任务只有一个定时器在处理。如下:

handleAutoPrint(){
  this.handleStop()
  this.timer = setInterval(()=>{
    this.task()
  },3000)
}

再在离开页面的生命周期函数中调用handleStop() 即可; 该问题,是在同事求助时候暴露的, 其中的第一个解决办法是在问题定位后,他自己的处理方式,比较具有代表性;

这类问题是我们平时开发比较容易忽视的点;尤其是对于性能优化接触少的同学;

最后的最后

下面请上我们今天的主角:有请小趴菜

小趴菜.jpeg