返回

防止因定时器遗留而引发的 JavaScript 应用故障:巧用最优实践

前端

对于我们工程师来说,如果没有JavaScript,我们几乎寸步难行,它已是整个软件行业的基石,我们几乎每天都在编写JavaScript代码。许多人经常会碰到这样一些错误,这些错误大多源于我们忘记清除setTimeout或setInterval生成的定时器。

当组件挂载后,每过一秒,sec 的值就会+1,看起来没有问题,但组件销毁时,setI…

setTimout与setInterval的优缺点分析

对于一个定时器而言,需要考虑的问题是其执行次数执行时机

执行次数 可以分为只执行一次重复执行 两种情况。

执行时机 可以分为指定时间执行指定时间间隔内重复执行 两种情况。

setInterval

setInterval(callback, 1000) //每隔1秒调用一次回调函数callback

setInterval可以根据设定的时间间隔重复执行回调函数callback,但无法设置回调函数只执行一次。

setTimeout

setTimeout(callback, 1000) //延迟1秒调用回调函数callback

setTimeout可以根据设定延迟时间只调用回调函数callback一次,但无法设定回调函数重复执行。

setTimout与setInterval使用中的注意事项

由上面的例子可知,选择使用setTimeout或setInterval应根据具体的业务场景需求。它们的使用都会带来好处,但同样也会带来隐患,下面我们就来分析一下使用setTimeout和setInterval要注意的问题。

setInterval

setInterval的回调函数是循环执行的,如果定时器的回调函数中没有对循环次数做出限制,则会一直执行下去,直到用户主动关闭网页或JavaScript代码本身因运行出错而停止,显然这并不是我们期望的结果。

通常,setTimeout函数的使用场景较为单一,就是在指定时间后触发一次事件。setInterval的用途则广泛得多,从简单的轮询,到复杂UI效果,甚至涉及流程控制和表单验证等场景,都会用到setInterval函数。

所以,我们可以得出如下结论:setTimeout的使用场景相对简单,而setInterval的使用场景更为复杂和多样。

setTimeout

setTimeout的回调函数只执行一次,且执行时间是固定的。显然,setTimeout不会像setInterval那样因为反复触发回调函数而导致资源浪费和性能问题。因此,在以下场景中,我们可以考虑使用setTimeout而不是setInterval:

  • 当我们要执行一个需要在指定时间后只执行一次的操作时。
  • 当我们要执行一个需要在指定时间后重复执行多次的操作,但可以控制重复执行的次数时。

虽然从表面上看,setTimeout的使用场景似乎更加受限,但实际场景中,满足上述要求的场景其实并不罕见。比如,以下场景就完全可以考虑使用setTimeout:

  • 页面加载完成后,只执行一次页面的初始化操作。
  • 表单提交时,只执行一次表单验证操作。
  • 滚动条拉动到底部时,只执行一次加载更多数据的操作。
  • 在一个模态框中,点击确定按钮时,只执行一次关闭模态框的操作。
  • 在一个定时任务系统中,只执行一次定时任务的操作。

如何强制实行 setTimeout 与 setInterval 的最佳实践

为了避免定时器遗留而引发的故障,我们需要遵循一些最佳实践:

  • 在组件销毁时清除定时器。
class MyClass {
  constructor() {
    this.timer = setTimeout(() => {
      // Do something
    }, 1000)
  }

  componentWillUnmount() {
    clearTimeout(this.timer)
  }
}
  • 使用 WeakMap 存储定时器。

WeakMap是一种弱引用集合,它可以将键值对存储在内存中,但不会阻止键值对被垃圾回收。这样,当组件被销毁时,WeakMap中的定时器也会被垃圾回收,从而避免了内存泄漏。

const timers = new WeakMap()

function setTimeout(callback, delay) {
  const timer = setTimeout(() => {
    callback()
    timers.delete(timer)
  }, delay)
  timers.set(timer, callback)
  return timer
}

function clearTimeout(timer) {
  timers.delete(timer)
  clearTimeout(timer)
}
  • 使用箭头函数创建定时器。

箭头函数不绑定自己的 this 值,因此在组件销毁时,箭头函数中的 this 值仍然指向组件实例,从而可以正确地清除定时器。

this.timer = setTimeout(() => {
  this.doSomething()
}, 1000)

通过遵循这些最佳实践,我们可以避免定时器遗留而引发的故障,从而确保应用程序的代码健壮性和性能得到优化。