settimeout需要清除吗_为什么用 setTimeout 替代 setInterval?

78eb2b30e82a6ff9c773a770ac5b7363.png

用 JavaScript 写个定时器,规定某段代码隔一段时间执行一次,第一反应肯定是用 setInterval.

例如规定每隔两百毫秒打印一次 hello, 就可以像下面这样写:

setInterval(() => {
    console.log('hello')    
}, 200)

上面的代码,每两百毫秒,将 console.log('hello') 推到事件队列。

setIntreavl 的第一个参数是要重复执行的函数,第二个参数是重复执行的时间间隔,以毫秒为单位;返回值是 Number 类型,一个唯一标志这个定时器的 ID,通过 clearInterval(intervalID) 可以清除这个定时器。

setInterval 的缺陷

考虑极端情况,假如定时器里面的代码需要进行大量的计算,或者是 DOM 操作。

这样一来,花的时间就比较长,有可能前一次代码还没有执行完,后一次代码就被添加到队列了。

假如时间间隔为 100 毫秒,要执行的代码需要 300 毫秒,如下图所示:

7ace9058ce680e3724f48c4c6385a878.png

图中只演示了定时器的前三次执行状况。

一开始执行 setInterval, 100 毫秒后将要执行的代码添加到队列。

100 毫秒时,执行代码进入队列,队列空闲,定时器内的代码执行。

200 毫秒时,第一次的定时器代码还在执行当中。第二次的定时器代码被推入事件队列,等待队列空闲,然后执行。

300 毫秒时,第一次的定时器代码还在执行中,第二次的定时器代码在事件队列末端等待执行。因为该定时器已经有第二次的代码在队列中等待了,所以这一次的代码不会被推入队列,被忽略了。

400 毫秒时,第一次的定时器代码执行完毕,队列空闲,下一个等待的代码执行,第二次的定时器代码开始执行。

捋一捋,这里的第一次的代码和第二次代码的间隔并没有预期的 100 毫米,而是第一次的执行完,第二次的立马执行了。因为第一的代码还没执行完,第二次的代码就已经在队列中等待了。

关于被忽略的第三次定时器代码,因为 300 毫秒时,这个定时器已经有第二次的代码在等待了,而只有当没有该定时器的代码在队列中时,该定时器新的代码才能去排队,所以第三次不会被添加到队列中。

由上可见,在这种极端情况下,setIntreval 实现不了需求。

用 setTimeout 实现 setInterval

setTimeout 可以实现 setInterval 的功能,并且不会出现上面的情况。

const repeat = (func, ms) => {
  setTimeout(() => {
    func()
    repeat(func, ms)
  }, ms)
}

上面的 repeat 接受两个参数,func 是要以间隔时间执行的函数,ms 代表间隔的毫秒数。

repeat 内部用 setTimeout 在指定的毫秒数 ms 之后,将匿名函数推入事件队列,匿名函数中包含要执行的 func , 以及 repeat(func, ms)。匿名函数执行时,先执行 func, 然后递归调用 repeat 来模拟 setInterval,递归调用的 repeat 中,又将执行 setTimeout, 在 ms 毫秒后,将下一次的定时器代码推入队列末端。

这里可以注意到,将下一次定时器代码推入队列时,上一次的代码无论如何都已经执行完了,所以不会重蹈 setInterval 的覆辙。

但是这种实现方式,不能清除定时器,还需要改造一下。

setTimeout 方式的改良

function Timer() {
  this.timeID = null
  this.func = null
}

Timer.prototype.repeat = function(func, ms) {
  if (this.func === null) {
    this.func = func
  }

  // 确保一个 Timer 实例只能重复一个 func
  if (this.func !== func) {
    return
  }

  this.timeID = setTimeout(() => {
    func()
    this.repeat(func, ms)
  }, ms)
}

Timer.prototype.clear = function() {
  clearTimeout(this.timeID)
}

const a = () => console.log('a')

const b = () => console.log('b')

const timer = new Timer()

timer.repeat(a, 1000)
timer.repeat(b, 1000) // 不会定时执行 b

上面的代码定义了构造函数 Timer, 其产生的实例有两个属性,timeID 用来存储 setTimeout 返回的值,也就是定时器的 ID; func 则用来存储需要被定时执行的函数。

Timerprototype 上定义了 repeat方法,和之前的 repeat 函数大致一样。只是在开头,将传入的 func 存到了 this.func. repeat 递归调用时,每执行一次 setTimeout , this.timeID 就会取得最新的定时器的 ID.

Timerprototype 上还定义了 clear 方法,用来清除定时器,。

注意到 repeatclear 两个方法,定义时用的是函数声明,而不是匿名函数。之所以这样做,是为了保证方法内 this指向 Timer 实例。如果用的是箭头函数,这两个方法里面的 this 将指向全局对象 Window, 更多和 this 有关的,可参考这篇博客。

接下来定义了两个简单的函数 ab.

new 一个 Timer 赋值给变量 timer, timer 执行 repeat 方法定时运行 ab .

可以发现,每隔大约一秒,就会执行一次 a, 控制台就会打印一次 a. 而 b 则不会定时重复执行。因为一个 Timer 实例只能重复执行一个函数。假如不作限制,ab 重复,那么在 clear 的时候,只能清除掉最后添加进来的重复代码的定时器 ID.

假如要重复执行 b, 只能再实例化一个 Timer.

总结

因为 setInterval 在一些情况下,会导致前后两次定时器代码的执行间隔产生不可预料的变化,甚至会跳过某次定时器代码的执行,所以可用 setTimeout 实现 setInterval 的功能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值