背景与问题
项目中,某些页面需要轮询请求更新页面,使用的是 setInterval。
但是我们发现,离开页面(组件)后,轮询并没有停止,而且是偶现,但我点击页面进入,然后很快切换出去的时候就会复现。第一直觉肯定是离开页面(组件)的时候,定时器没有清理。但排查代码之后发现,是有清理定时器的,类似代码如下:
js
mounted() {
// 执行其他逻辑
this.timer = setInterval(() => {
console.log("我刷新了");
}, 5000);
},
unmounted() {
clearInterval(this.timer);
},
那会是什么问题呢?仔细一看,在启动定时器之前还有一些其他逻辑,比如请求一个接口,需要等待返回才启动定时器。大概逻辑如下:
diff
async mounted() {
// Perform other logic
+ // Execute a request
+ await new Promise((resolve) => {
+ setTimeout(() => resolve(), 1000)
+ });
+ console.log('First implementation');
this.timer = setInterval(() => {
console.log("I'm refreshing");
}, 1000);
},
unmounted() {
clearInterval(this.timer);
},
示例代码可以看这里,示例代码1。
所以这里的问题是,在还没启用定时器的时候,就已经离开这个页面(组件)了,而在我们离开页面(组件)之后,内存中的代码才开启定时器。
为了更好的理解,可以看以下流程:
如何解决
方法一:立即启动定时器
既然是因为定时器启动晚导致的,那么进入页面(组件)就直接启动可能也是一种思路。但这种很明显缺点,不一定满足业务需求。因为定时器里面的逻辑很可能依赖前置一些耗时操作(比如请求得到的数据)。所以这个不建议使用。
方法二:通过标识离开页面(组件)
这个方法也很简单,就是定义一个辨识,比如 isLeavePage,初始化为 false,在离开页面(组件)的时候设置为 true。我们就可以在定时器逻辑中进行判断,如果为 true,则直接清除定时器。以下为简单示例代码:
diff
async mounted() {
// Perform other logic
// Execute a request
await new Promise((resolve) => {
setTimeout(() => resolve(), 1000);
});
console.log("First implementation");
this.timer = setInterval(() => {
+ if (this.isLeavePage && this.timer) {
+ clearInterval(this.timer);
+ return;
+ }
console.log("I'm refreshing");
}, 1000);
},
unmounted() {
+ this.isLeavePage = true;
clearInterval(this.timer);
},
缺点是有一定的代码侵入性,但你可以看到,代码量还是比较少的。
方法三:不使用定时器
这种就是从根源上避开问题了,比如轮询的场景,可以使用类似 WebSockets / Server-Sent Events 等技术替代。这个这里不展开了。
可能的方法四:如何离开页面(组件)之后,不执行内存中代码执行?
上面的问题本质上还是离开页面(组件)之后,内存中代码依旧执行的问题,如果能做到销毁页面(组件)之后就不再执行内存中的代码,是不是就可以了呢?可惜目前我并没有看到 JS 可以做到这一点。
探索:轮询 setTimeout VS setInterval
这个章节实际上跟这个问题关系不大,如果不感兴趣,可以忽略!
大家看到,以上示例轮询,我使用的是 setInterval,实际上这可能会有一定问题的。
setTimeout 和 setInterval 将任务加入任务队列的区别:每个 setTimeout 产生的任务会直接 push 到任务队列中;而 setInterval 在每次把任务 push 到任务队列前,都要进行一下判断(看上次的任务是否仍在队列中,如果有则不添加,没有则添加)。
这里假设,我们使用 setInterval 启动了一个间隔 100ms 的定时器,但实际上每次执行定时器中的逻辑时间超过 100ms。它的执行会如下所示:
-
问题一:使用 setInterval 时,某些间隔会被跳过。从上图中可以看出,当 setInterval 要将第三个任务加入队列的时候(T3),T2 任务还没执行完,还处于队列中,这个时候,它就会直接跳过。但假如是 setTimeout 的话,它依旧会加入到队列中,不管之前是否有任务。
-
问题二:可能多个定时器会连续执行。另外还有一个小点就是 T1 执行完之后,直接就执行了 T2,这种连续执行可能不一定符合我们的预期。
我们可以直接使用 setTimeout 模拟 setInterval:
js
let timer = null
interval(func, wait){
let interv = function(){
func.call(null);
timer=setTimeout(interv, wait);
};
timer= setTimeout(interv, wait);
},
如果还有疑问,推荐可以看看这篇文章(以上结论参考自此文):为什么要用 setTimeout 模拟 setInterval ?
总结
定时器,我们项目中经常会用到,而且看网上示例也都是在页面(组件)离开的时候就清除定时器即可。
但实际上,可能会因为前置操作逻辑比较耗时,导致用户离开页面(组件)之后,才开启定时器,这样可能会导致你的页面出现不及预期的表现。
本文分享了几种解决思路,希望对你有帮助,如果有其他的想法,也欢迎讨论!