使用setTimeout函数实现setInterval函数的效果,利用Promise来控制异步代码的执行顺序

如题,今天回顾了《JS高级程序设计》书中关于setTimeout函数和setInterval函数的描述,书中介绍到:

可见,在使用超时调用时,没有必要跟踪超时调用 ID,因为每次执行代码之后,如果不再设置另一次超时调用,调用就会自行停止。一般认为,使用超时调用来模拟间歇调用的是一种最佳模式。在开发环境下,很少使用真正的间歇调用,原因是后一个间歇调用可能会在前一个间歇调用结束之前启动。

而像前面示例中那样使用超时调用,则完全可以避免这一点。所以,最好不要使用间歇调用。

 使用setInerval函数可能导致后一个间歇调用会在前一个间歇调用之前便启动,这句话还是比较好理解的,比如说如果调用的是一个比较耗时的异步操作的话,那么就有可能出现后面的调用函数在前一个调用还没有执行完之前就启用,此时,JS 引擎的解决办法是:

js引擎对这个问题的解决方法就是,当使用setInterval()时,仅当没有该定时器的任何其他代码实例时,才将定时器代码添加到队列中。这确保了定时器代码加入到队列中的最小时间间隔为指定间隔。意思就是,如果当事件队列当中,已经存在了定时器的回调函数,即使已经到了规定的间隔时间,也不会再把这个时间点的定时器回调函数放到事件队列当中,定时器依旧运行。当下一个约定时间又到了,如果事件队列当中依然存在定时器的回调函数,这个时间点的定时器回调函数也不会放进事件队列....

那么 如果利用超时调用(setTimeout)来模拟间歇调用会怎样呢?

在这个例子中,变量 num 每半秒钟递增一次,当递增到最大值时就会取消先前设定的间歇调用。这 个模式也可以使用超时调用来实现,如下所示。

var num = 0; 
var max = 10;
 
function incrementNumber() { 
 num++;
/**
    在这里加入需要执行的业务代码
*/ 
 //如果执行次数未达到 max 设定的值,则设置另一次超时调用
 if (num < max) { 
 setTimeout(incrementNumber, 500); 
 } else { 
 alert("Done"); 
 } 
} 

setTimeout(incrementNumber, 500);

这是红宝书给的一个例子,一般我们可以考虑在num++这句话的前后加上我们需要间歇执行的逻辑代码,这种写法有一个好处就是:如果需要执行的业务逻辑代码是同步代码的话,那么必然不会出现“后一个间歇调用可能会在前一个间歇调用结束之前启动”这种情况,而如果需要执行的业务代码有异步操作的话(比如ajax),那么业务代码同样会执行预期的次数,而不会出现setInterval函数的那种重复回调不放进事件队列的情况。

但是即使是这样,异步的业务代码的执行顺序仍然是不可预测的(如果是固定时间延时操作之类的可以预测),尤其执行的是ajax的异步代码,其返回结果的时间是不确定的。下面是一个例子:

    test (num, max) {
      num++
      
        setTimeout(() => {
          console.log(num)
        }, 15000)
      
        if (num < max) {
          setTimeout(() => {
            console.log('aaa')
            test(num, max)
          }, 3000)
        } else {
          alert('done')
        }
    }

setTimeout(() => {
   test(0, 10)
}, 3000)

在这里我们认为这个间隔15秒之后执行的延时代码是需要定时执行的业务代码:

setTimeout(() => {
      console.log(num)
 }, 15000)

由于是固定15秒之后执行这时候最终的输出结果我们是可以预测的,最终的结果如下图:

在这里num是从21s之后开始每隔3秒执行的,当num为5的时候实际上此时再主线程执行的num已经等于9了,之后便等于10跳出乐递归执行,但是延时函数已经执行过了10遍,在事件队列中已经加入了10个业务代码的回调,于是之后还会陆续的输出6 7 8 9 10完毕

因为业务代码中是固定15s之后执行的,所以即使是异步的代码我们仍然可以预测其执行结果和顺序,但是如果是ajax请求或者延时的时间是随机的话,这种递归调用setTimeout函数的写法最终会让我们无法预料异步代码的执行顺序,此时如果我们是希望在每一次执行完了业务逻辑代码之后才能进行下一个业务逻辑代码的执行,也就是同步的执行这些代码的话,那么可以使用Promise来改写:

   test (num, max) {
      num++
      new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve()
          console.log(num)
        }, 15000)
      }).then(r => {
        if (num < max) {
          setTimeout(() => {
            console.log('aaa')
            this.test(num, max)
          }, 3000)
        } else {
          alert('done')
        }
      })
    }

    setTimeout(() => {
      test(0, 10)
    }, 3000)

这时候代码便会以同步的顺序执行了:

这种写法适合用于定时轮询时。当然最好加上reject时的处理。

总结:相比于setInterval,使用setTimeout函数更加稳定,不会出现回调函数可能不放进事件队列的情况——这也许是红宝书中“则完全可以避免这一点”的解释吧。

参考: https://blog.csdn.net/chiuwingyan/article/details/80322289

发布了24 篇原创文章 · 获赞 20 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/a715167986/article/details/96434248
今日推荐