递归模拟定时器后clearTimeout失效问题,轻度探讨clear行为

165 阅读2分钟

起因

(百度翻遍都没找到,只能自己解决了)

对于长时间请求,使用interval不稳定,需要用settimeout结合递归来实现请求发送完成后再请求的效果

时隔两个月的补充,因为又栽倒在了同一个问题上

  • 模拟代码
        var myIntervalTimer = null
        const myInterval = (fn, delay) => {
            clearInterval(myIntervalTimer)
            myIntervalTimer = setTimeout(() => {
                fn().then(() => {
                    console.log('自定义延时器执行')
                    myInterval(fn, delay)
                })
            }, delay)
        }
        myInterval(async () => {
            console.log('模拟请求');
            await new Promise((resolve) => setTimeout(resolve, 5000))
        }, 2000)

        setTimeout(() => {
            clearInterval(myIntervalTimer)
        }, 10000);

理解到在执行最后一次请求时,删除定时器已经晚了,请求已经执行

结果还是要把方法置空或者设置一个标志,现在封装成对象方便以后再用

        class myInterval {
            constructor(fn, time) {
                this.end = false
                // 定义一个递归函数,持续调用定时器
                var execute = async (fn, time) => {
                    await fn();
                    if (!this.end) {
                        // 在函数执行完毕后才再起用定时器
                        setTimeout(function () {
                            console.log('自定义延时器执行')
                            execute(fn, time);
                        }, time);
                    }
                };
                execute(fn, time);
            }
            clear() {
                this.end = true
            }
        }

        let myIntervalTimer = new myInterval(async () => {
            console.log('模拟请求');
            await new Promise((resolve) => setTimeout(resolve, 5000))
        }, 2000)

        setTimeout(() => {
            myIntervalTimer.clear()
        }, 10000);

首先是对clearTimeout和clearInterval的理解,来自mdn:

  • 都是根据id清除
  • clearTimeout:取消了建立的定时器(我的理解是在任务放入任务队列前进行销毁)
  • clearInterval:取消设置的重复定时任务。

所以有两种引发清除失败的情况:

  1. 对于setInterval,如果重复赋值,离开组件再清除的话,只会清除最后一次的id,所以要在设置定时器前使用clearInterval
  2. 对于setTimeout模拟setInterval,销毁最后一次定时器后也会因为回调继续执行,因为这里用到的回调是异步的,删除定时器前,then回调就已经推入队列了,并且匿名函数闭包持有回调函数,导致轮询继续,解决方案是清除定时器时将方法置空。
  3. 补充:正确的做法是使用同步,总之理解原理就好找bug了

原始代码:

const updateData = () => {
        clearTimeout(timer);
        this.timer = setTimeout(async () => {
          const res = await request.request.getCloudDevice();
          this.tableData = res.data?.data;
          this.total = this.tableData.length;
          this.tableList = this.tableData;
          this.searchItem(this.inputKey);
          updateData();
        }, 5000);
      };
updateData();

beforeDestroy() {
    this.timer && clearInterval(this.timer);
    this.timer = null;
  },

修复代码:

let timer;
let updateData = () => {
    timer = setTimeout(async () => {
      const res = await request.request.getCloudDevice();
      this.tableData = res.data?.data;
      this.total = this.tableData.length;
      this.tableList = this.tableData;
      this.searchItem(this.inputKey);
      updateData();
    }, 5000);
};
updateData();
//再通过事件监听,监听到 组件销毁 后,再执行关闭计时器。
this.$once('hook:beforeDestroy', () => {
    clearTimeout(timer);
    updateData = null;
});