setTimeout() 和 setIntervale() 小结🥥

598 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第23天,点击查看活动详情

setTimeout()

JavaScript语言是单线程语言,它有一个叫做执行队列的东西来决定代码的执行顺序,而定时器的作用是:在特定的时间后将代码插入到执行队列。

这里要特别理解:定时器setTimeout(function, Interval)这里的Interval是指当Interval个单位时间过去之后将代码function插入到执行队列中,而不是过去Interval个单位时间之后执行function代码。也就说明代码的执行的时间将大于等于Interval。

使用 setTimeout 设置循环任务:当 num === 10 的时候,任务完成

let num = 0;
let max = 10;
let incrementNumber = function () {
  num++
  console.log(num)
  if (num < max) {
    setTimeout(incrementNumber, 500)
  } else {
    alert('任务完成,奖励自己一朵小红花')
  }
}
setTimeout(incrementNumber, 500);

在这里插入图片描述

有了 setTimeout,那 setInterval 是否可以完成同样的任务呢?

let num = 0, intervalId = null;
let max = 10;

let incrementNumber = function () {
  num++
  console.log(num)
  if (num === max) {
    clearInterval(intervalId)
    alert('任务完成,奖励自己一朵小红花')
  }
};
intervalId = setInterval(incrementNumber, 500);

但是呢,一般来说,setIntervale() 在实践中很少会在生产环境下使用

setInterval() 接收两个参数:要执行的代码(字符串或函数),以及把下一次执行定时代码的任务添加到队列要等待的时间(毫秒)。

这里的关键点是,第二个参数,也就是间隔时间,指的是向队列添加新任务之前等待的时间。比如,调用 setInterval() 的时间为 10:00:00,间隔时间为 3000 毫秒。这意 味着 10:00:03 时,浏览器会把任务添加到执行队列。浏览器不关心这个任务什么时候执行或者执行要花多长时间。因为一个任务结束和下一个任务开始之间的时间间隔是无法保证的,有些循环定时任务可能会因此而被跳过。

如何消除不准确

动态计算时差 (仅针对循环定时,只起修正作用 )

  • 在定时器开始前和运行时动态获取当前时间,在设置下一次定时时长时,在期望值基础上减去当前时延,以获得相对精准的定时运行效果。
  • 此方法仅能消除setInterval()长时间运行造成的误差累计,但无法消除单个定时器执行延迟问题。
var count = count2 = 0;
var runTime, runTime2;
var startTime, startTime2 = performance.now();//获取当前时间

//普通任务-对比
setInterval(function () {
	runTime2 = performance.now();
	++count2;
	let time = runTime2 - (startTime2 + count2 * 1000)
	console.log("普通任务", count2 + ' --- 延时:' + time + ' 毫秒');
}, 1000);

//动态计算时长
function func() {
	runTime = performance.now();
	++count;
	let time = runTime - (startTime + count * 1000)
	console.log("优化任务", count2 + ' --- 延时:' + time + ' 毫秒');
	//动态修正定时时间
	t = setTimeout(func, 1000 - time);
}
startTime = performance.now();
var t = setTimeout(func, 1000);

//耗时任务
setInterval(function () {
	let i = 0;
	while (++i < 100000000);
}, 0);

在这里插入图片描述

相差其实不大

使用 Web Worker

Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。

将该文件直接放入浏览器中运行是会报错的,可以使用 live-server 在本地起一个服务器,这样 worker 才会正常运行

<!-- index.html -->
<html>
<meta charset="utf-8">

<body>
  <script type="text/javascript">
    var count = 0;
    var runTime;

    //performance.now()相对Date.now()精度更高,并且不会受系统程序堵塞的影响。
    //API:https://developer.mozilla.org/zh-CN/docs/Web/API/Performance/now
    var startTime = performance.now(); //获取当前时间 

    //普通任务-对比测试
    setInterval(function () {
      runTime = performance.now();
      ++count;
      console.log("普通任务", count + ' --- 普通任务延时:' + (runTime - (startTime + 1000)) + ' 毫秒');
      startTime = performance.now();
    }, 1000);

    //耗时任务
    setInterval(function () {
      let i = 0;
      while (i++ < 100000000);
    }, 0);

    // worker 解决方案
    let worker = new Worker('./worker.js');
  </script>
</body>

</html>
// worker.js
var count = 0;
var runTime;
var startTime = performance.now();

setInterval(function () {
  runTime = performance.now();
  ++count;
  console.log("worker任务", count + ' --- 延时:' + (runTime - (startTime + 1000)) + ' 毫秒');
  startTime = performance.now();
}, 1000);

requestAnimationFrame

requestAnimationFrame是浏览器用于定时循环操作的一个接口,类似于setTimeout,主要用途是按帧对网页进行重绘。requestAnimationFrame 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧,即是时间间隔为1000/60=16.7ms。 requestAnimationFrame 与 setTimeout相比,最大的优势在于requestAnimationFrame是由系统来决定回调函数的执行时机,充分利用显示器的刷新机制,比较节省系统资源。当页面处于不可见或不可用状态时,浏览器就会停止动画,这意味着更少的CPU和更少的内存消耗。

问题:大家都知道 setTimeout 属于宏任务,那么 requestAnimationFrame 是属于宏任务还是微任务? 先来看一道题:

setTimeout(() => {
  console.log(1)
});
requestAnimationFrame(() => {
  console.log(2)
})
setTimeout(() => {
  console.log(4)
})

是 2 1 4 ?还是 1 4 2。 别猜,去浏览器中指行一下,你会发现一下子是 2 1 4,一下子又是 1 4 2。这是咋回事,于是你懵逼了。。。。

它既不是宏任务,又不是微任务。 一个 setTimeout 在 requestAnimationFrame 的前面,它们里面的两个回调的顺序是不确定的。

场景:每隔10ms设置图像向右移动1px

<!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>
    .box1 {
      width: 100px;
      height: 100px;
      background-color: aquamarine;
    }

    .box2 {
      margin-top: 100px;
      width: 100px;
      height: 100px;
      background-color: aquamarine;
    }
  </style>
</head>

<body>
  <div class="box1"></div>
  <div class="box2"></div>
  <script>
    let box1 = document.querySelector('.box1');
    let box2 = document.querySelector('.box2');

    // setTimeout
    let num = 0;
    let max = 10000000;
    let degrees1 = 0;

    let incrementNumber = function () {
      num++
      box1.style.transform = "translateX(" + degrees + "px)";
      degrees1 = degrees1 + 1
      if (num < max) {
        setTimeout(incrementNumber, 10)
      }
    }
    setTimeout(incrementNumber, 10);

    // requestAnimationFrame
    var degrees = 0;
    function update() {
      box2.style.transform = "translateX(" + degrees + "px)";
      degrees = degrees + 1;
      window.requestAnimationFrame(update);
    }
    window.requestAnimationFrame(update)
  </script>
</body>

</html>

在浏览器预览中可以看到,两个方块都是匀速前进的。 这时,在绘制之前,在其中添加耗时的任务

<!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>
    .box1 {
      width: 100px;
      height: 100px;
      background-color: aquamarine;
    }

    .box2 {
      margin-top: 100px;
      width: 100px;
      height: 100px;
      background-color: aquamarine;
    }
  </style>
</head>

<body>
  <div class="box1"></div>
  <div class="box2"></div>
  <script>
    let box1 = document.querySelector('.box1');
    let box2 = document.querySelector('.box2');

    let num = 0;
    let max = 10000000;
    let degrees1 = 0;

    let incrementNumber = function () {
      num++
      box1.style.transform = "translateX(" + degrees + "px)";
      setInterval(function () {
        let i = 0;
        while (++i < 100000000);
      }, 0);
      degrees1 = degrees1 + 1
      if (num < max) {
        setTimeout(incrementNumber, 10)
      }
    }

    setTimeout(incrementNumber, 10);


    var degrees = 0;
    function update() {
      box2.style.transform = "translateX(" + degrees + "px)";
      setInterval(function () {
        let i = 0;
        while (++i < 100000000);
      }, 0);
      degrees = degrees + 1;
      window.requestAnimationFrame(update);
    }
    window.requestAnimationFrame(update)
  </script>
</body>

</html>

在浏览器中,使用 setTimeout 的方块会出现卡顿,但是使用 requestAnimationFrame 则是非常顺畅。

参考文章:

requestAnimationFrame回调在HTML的Event Loop中是一个宏任务么?

【前端面试】requestAnimationFrame 是宏任务吗