js基础 - 定时器

137 阅读4分钟

有时我们并不想立即执行一个函数,而是等待特定一段时间之后再执行,我们称之为“计划调用(scheduling a call)”

目前有两种方式可以实现:

  1. setTimeout 允许我们将函数推迟到一段时间间隔之后再执行
  2. setInterval 允许我们重复运行一个函数,从一段时间间隔之后开始运行,之后以该时间间隔连续重复运行该函数

并且通常情况下有提供对应的取消方法:

  1. clearTimeout: 取消setTimeout的定时器
  2. clearInterval: 取消setInterval的定时器

setTimeoutsetInterval不仅仅可以在浏览器中使用,node环境中也实现了对应的方法,也就意味着这两个方法,也可以在node环境中使用

setTimeoutsetInterval都属于宏任务,每一个定时器回调函数都会被依次加入到宿主环境的宏任务队列中

等待对应的微任务队列执行完毕后,这些定时器回调函数会依次从宏任务队列中被取出并依次执行

setTimeout

# 参数1 --- 想要执行的函数或代码字符串 --- 因为历史原因可以传入字符串,但是不被推荐
# 参数2 --- 执行前的延时,以毫秒为单位(1000 毫秒 = 1 秒),默认值是 0
# 参数3 --- 要传入被执行函数(或代码字符串)的参数列表

# 返回值 --- setTimeout 在调用时会返回一个“定时器标识符(timer identifier)”,我们可以使用它来取消执行
# 返回值的类型是Number 即 typeof timeoutID -> Number
const timeoutID = scope.setTimeout(function[, delay, arg1, arg2, ...])

setInterval

# setInterval参数和返回值 和 setTimeout 一致
# 唯一的区别是 和 setTimeout 只执行一次不同,setInterval 是每间隔给定的时间周期性执行
# setInterval也会返回一个“定时器标识符(timer identifier)”,我们可以通过clearInterval来取消这个定时器
const intervalID = setInterval(func, [delay, arg1, arg2, ...])

示例

function sum(num1, num2) {
  console.log(num1 + num2)
}

// 第一个参数为定时器函数
// 第二个参数为定时器间隔
// 第三个参数和第四个参数即为需要移次传递给sum函数的参数

// 定时器方法会返回一个唯一的number类型的值,以表示当前定时器ID值
const timerId = setInterval(sum, 1000, 10, 20)

// 过3s 取消interval定时器
// 取消定时器方法需要传递定时器ID
// 如果定时器ID不存在,则该方法静默失效
// 如果定时器ID存在,则会清除对应的定时器
setTimeout(() => clearInterval(timerId), 3000)

requestAnimationFrame

setInterval和setTimeout在执行定时任务的时候,有一个致命缺点

setInterval和setTimeout是宏任务队列,在定时器时间达到,需要执行传入的回调之前,会先判断微任务队列中是否存在对应需要执行的任务

这就导致了setInterval和setTimeout的任务执行时间是不精准的

如果微任务中一直有未处理完成的任务,那么setInterval和setTimeout的回调函数就有可能不会在指定时间内触发回调

如果想要更加平稳和更加精准的定时执行某个任务的话,可以使用requestAnimationFrame函数

目前,绝大多数的浏览器的刷新率是60HZ,也就是1000ms刷新60次,即大约16.67ms刷新一次页面内容

如果我们将可能造成页面回流和重绘的操作放在浏览器刷新的时候一起执行,那么就可以尽最大可能去减少浏览器的重绘和回流

而requestAnimationFrame的使用和setTimeout的使用是基本一致的,只不过对于requestAnimationFrame方法,我们不需要去指定对应的时间

因为requestAnimationFrame方法传入的回调,会在浏览器刷新之前被回调,也就是说requestAnimationFrame中传入的回调一秒内会执行大约60次

同时因为requestAnimationFrame的执行交给了浏览器来进行完成

所以浏览器对requestAnimationFrame的回调函数的调用进行了如下优化

  1. 对于不是激活状态的页面,requestAnimationFrame对应的回调会停止执行,知道页面恢复激活状态
  2. 对于隐藏,不可见的元素,requestAnimationFrame会自动失效,因为对看不见的元素执行动画是没有任何意义的
const start = Date.now()

requestAnimationFrame(function fn() {
  const end = Date.now()

  if (end - start >= 1000) {
    return
  }

  console.log('requestAnimationFrame被回调了')

  // 因为requestAnimationFrame的默认行为类似于setTimeout
  // 所以如果要使用requestAnimationFrame模拟setInterval,就需要使用递归
  requestAnimationFrame(fn)
})

和setTimeout一样,requestAnimationFrame 也会返回一个number类型的timer

如果我们要在requestAnimationFrame执行之前清除对应的回调,可以使用cancelAnimationFrame方法

const timer = requestAnimationFrame(function fn() {

  console.log('requestAnimationFrame被回调了')

  requestAnimationFrame(fn)
})

// 使用timer清除requestAnimationFrame方法
cancelAnimationFrame(timer)

示例 - 使用requestAnimationFrame来模拟进度条

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .process {
      width: 0;
      height: 60px;
      line-height: 60px;
      background-color: red;
      cursor: pointer;
    }
  </style>
</head>
<body>
  <div class="process">0%</div>

  <script>
    const processEl = document.querySelector('.process')

    processEl.onclick = () => {
      let timer = requestAnimationFrame(function fn() {
        let width = 0

        if (parseInt(processEl.style.width)) {
          width = parseInt(processEl.style.width)
        }

        if (width < 500) {
          width += 5
        } else {
          cancelAnimationFrame(timer)
        }


        processEl.style.width = width +  'px'
        processEl.innerHTML = parseInt(width / 500 * 100) + '%'

        timer = requestAnimationFrame(fn)
      })
    }
  </script>
</body>
</html>