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

背景与问题

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

总结

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

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

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

相关推荐
inksci1 小时前
Vue 3 中通过 this. 调用 setup 暴露的函数
前端·javascript·vue.js
未来之窗软件服务2 小时前
monaco-editor 微软开源本地WEB-IDE-自定义自己的开发工具
开发语言·前端·javascript·编辑器·仙盟创梦ide
白白糖2 小时前
二、HTML
前端·html
子燕若水2 小时前
continue dev 的配置
java·服务器·前端
学习HCIA的小白2 小时前
关于浏览器对于HTML实体编码,urlencode,Unicode解析
前端·html
南方下小雨2 小时前
基于Spring Boot + Vue 项目中引入deepseek方法
vue.js·spring boot·后端
向明天乄2 小时前
Vue3 后台管理系统模板
前端·vue.js
香蕉可乐荷包蛋3 小时前
vue 常见ui库对比(element、ant、antV等)
javascript·vue.js·ui
彩旗工作室3 小时前
Web应用开发指南
前端
孙俊熙4 小时前
react中封装一个预览.doc和.docx文件的组件
前端·react.js·前端框架