页面中使用到定时器的注意啦📢这个坑可能你们也会有!

背景与问题

项目中,某些页面需要轮询请求更新页面,使用的是 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 ?

总结

定时器,我们项目中经常会用到,而且看网上示例也都是在页面(组件)离开的时候就清除定时器即可。

但实际上,可能会因为前置操作逻辑比较耗时,导致用户离开页面(组件)之后,才开启定时器,这样可能会导致你的页面出现不及预期的表现。

本文分享了几种解决思路,希望对你有帮助,如果有其他的想法,也欢迎讨论!

相关推荐
图扑软件3 小时前
可视化重塑汽车展示平台新体验
前端·javascript·人工智能·数字孪生·可视化·智慧交通·智慧出行
Xudde.4 小时前
HTML中最基本的东西
前端·css·笔记·html
NoneCoder5 小时前
JavaScript系列(26)--安全编程实践详解
开发语言·javascript·安全
杨荧5 小时前
【开源免费】基于Vue和SpringBoot的林业产品推荐系统(附论文)
前端·javascript·vue.js·spring boot·开源
宏夏c6 小时前
【Vue】let、const、var的区别、适用场景
开发语言·javascript·ecmascript
光影少年6 小时前
前端进程和线程及介绍
前端·javascript
涔溪6 小时前
JS二叉树是什么?二叉树的特性
java·javascript·数据结构
贩卖纯净水.6 小时前
JS后盾人--再一次的走进JS?
开发语言·javascript·ecmascript
Franciz小测测6 小时前
VUE3 + Ant Design Vue4 开发笔记
前端·vue.js·vue