距离上一次的文章《了解策略模式(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() 即可; 该问题,是在同事求助时候暴露的, 其中的第一个解决办法是在问题定位后,他自己的处理方式,比较具有代表性;
这类问题是我们平时开发比较容易忽视的点;尤其是对于性能优化接触少的同学;
最后的最后
下面请上我们今天的主角:有请小趴菜