setInterval 定时任务执行时间不准验证

一般在处理定时任务的时候都使用setInterval间隔定时调用任务。

js 复制代码
setInterval(() => {
  console.log("interval");
}, 2 * 1000);

我们定义的是两秒执行一次,但是浏览器实际执行的间隔时间只多不少。这是由于浏览器执行 JS 是单线程模式,使用setInterval定时执行的回调只会在线程空闲时调用。

通过增加时间记录,对比每次调用的间隔并打印:

js 复制代码
// 记录最后一次调用的时间
let lastCall = Date.now();

setInterval(() => {
  let now = Date.now();

  // 由最后一次调用时间和当前时间计算出间隔时间
  let delay = now - lastCall;
  lastCall = now;
  console.log("interval---", delay);
}, 2 * 1000);

看着执行间隔时间还行,没差多少。因为我们的测试页面什么都没有,主线程一直空着,没有被影响.

我们来增加一个按钮,然后点击事件后随机生成数字,然后排序。目的是为了占用主线程,看定时任务的执行时间

js 复制代码
function handleClick(event) {
  let arr = [];
  for (let i = 0; i < 1000000; i++) {
    let num = Math.random() * 10000;
    arr.push(num);
  }
  arr.sort();
  console.log("-------click---------");
}

本来是几千、几万条数据进行测试的,发现执行速度很快,不能长时间占用。就直接加大数据数量,然后连续点击按钮十几下。

可以看看对比效果:

可以看到我连续点击十多次,导致主线程一直被占用。主线程空闲后执行定时任务,第一个任务执行时间由 10ms,第二任务执行只有 1.3 秒。这是为什么呢?

因为setInterval的执行不在乎主线程有没有空,它只会按照间隔触发函数执行,而这个回调任务会被加入到任务队列中。等待主线程空闲时出队列调用。

大于间隔时间是主线程被占用,任务等待执行,导致整个时间超了;小于间隔时间是线程占用时间过长,任务执行队列中已经存在多个等待执行的任务,导致上一个任务刚执行完,下一个任务就执行了。

那么我们定时任务就不能按照间隔正常调用,因为我们无法改变 JS 单线程的事实。

但可以解决一下间隔时间小于指定的时间间隔的问题。也就是每次执行回调时间都尽可能的>=指定的时间间隔。

setTimeout

使用setTimeout实现一个自定义循环,在循环每次结束后,重新设置一个定时器,而不是预先固定间隔。

js 复制代码
let lastCall = Date.now();
function intervalCall() {
  let now = Date.now();

  let delay = now - lastCall;
  lastCall = now;
  console.log("interval---", delay);
  setTimeout(intervalCall, 2 * 1000);
}
intervalCall();

测试,可以看到在主线程空闲之后的两次任务调用中,第一个任务执行超过 10 秒,第二个任务 2 秒多。这就解决了间隔执行时间小于指定时间间隔的问题。

补偿执行时间

什么叫执行时间,就是你的回调业务逻辑执行的时间,我们之前验证了间隔时间不准确的问题,这个没法解决。但可以考虑优化调整一下下次任务执行时的间隔。

如果回调业务逻辑里很复杂,很耗时,那执行到最后时重置的定时器执行间隔已经不准了。

js 复制代码
let lastCall = Date.now();
function intervalCall() {
  let now = Date.now();

  let delay = now - lastCall;
  lastCall = now;
  console.log("interval---", delay);

  // 耗时
  let arr = [];
  for (let i = 0; i < 1000000; i++) {
    let num = Math.random() * 10000;
    arr.push(num);
  }
  arr.sort();
  setTimeout(intervalCall, 2 * 1000);
}
intervalCall();

可以看到由于耗时的任务,导致每次的间隔调用都在 3、4 秒了,所以这部分执行时间我们要补偿回来。

js 复制代码
function intervalCall() {
  let now = Date.now();

  //... 业务逻辑

  // 执行结束时间
  let handlerTime = now - Date.now();

  // 下一次的间隔时间
  let intervalTime = Math.max(0, 20000 - handlerTime);

  setTimeout(intervalCall, intervalTime);
}

我们在执行开始记录了执行的开始时间now,在业务逻辑执行完后记录执行完毕的时间handlerTime,然后计算出执行时间,并在下次定时中减掉执行时间。但是有可能出现执行时间大于函数执行间隔时间,所以Math.max(0, 20000 - handlerTime),最短间隔 0m,直接执行。

可以看到测试数据,比没处理完好很多,基本都在 2 秒多一点。

上面是使用了setTimeout进行时间补偿,那使用setInterval呢,使用setInterval后任务肯定是定时去调用回调的,会出现之前主线程被占用,导致任务队列中存在多个定时任务,主线程空闲后,直接执行的话两个任务之间的间隔就不足设定的间隔了。

js 复制代码
let lastCall = Date.now();
setInterval(() => {
  let now = Date.now();

  let delay = now - lastCall;
  if (delay < 2000) {
    let intervalTime = 2000 - delay;
    setTimeout(() => {
      let now = Date.now();
      let delay = now - lastCall;
      console.log("补偿interval---", delay);
      lastCall = now;
    }, intervalTime);
    return;
  }
  console.log("interval---", delay);
  lastCall = now;
}, 2 * 1000);

计算了间隔时间delay,如果间隔时间还未到设定时间,则重新定制一个定时器setTimeout来执行任务。

setInterval不需要考虑任务执行时间,本身就是按照间隔时间去执行的。

重新执行测试主线程被占用时,后续任务执行情况。可以看到主线程被占用的第二个回调任务和第一个任务执行间隔在 2 秒多,不会少于间隔时间。这也尽可能保证按照设定间隔执行任务。

requestAnimationFrame 浏览器重绘之前执行

接受一个回调方法,在浏览器重绘之前调用一次。回调函数执行次数通常是每秒 60 次,它与浏览器屏幕刷新次数相匹配;在后台标签页或隐藏的 iframe 中,会停止执行。时间上可能会比较精确一点。

js 复制代码
let lastCall = Date.now();

function intervalCall() {
  let now = Date.now();
  let delay = now - lastCall;
  if (delay >= 2000) {
    // ...
    console.log("interval---", delay);
    lastCall = now;
  }

  requestAnimationFrame(intervalCall);
}
requestAnimationFrame(intervalCall);

在我点击按钮占用主线程时,居然见缝插针执行了一个任务。看来每秒 60 次的调用中还是很快的。

performance.now()精确度可达微秒级

改变Date.now使用performance.now来计算间隔时间

js 复制代码
// let lastCall = Date.now();
let lastCall = performance.now();

function intervalCall() {
  // let now = Date.now();
  let now = performance.now();
  let delay = now - lastCall;
  // console.log(now, "----", delay);
  if (delay >= 2000) {
    // ...
    console.log("interval---", delay);
    lastCall = now;
  }

  requestAnimationFrame(intervalCall);
}
requestAnimationFrame(intervalCall);

但是由于安全问题,这个 API 可能会跟浏览器的设置而废弃。实际上并不是高精度的,为了防范定时攻击和对指纹的保护,降低了原来的高精度。

优化耗时任务

上面测试了因为时间不准都是因为任务执行耗时,导致主线程被占用。从而加大了延时调用的时间,那么可以从优化执行耗时任务探索,尽可能的加快任务执行。

  • 避免昂贵的计算和 DOM 操作。
  • 使用Web Workers,在后台线程中处理任务
  • 对于一些操作,可以使用节流、防抖来限制在指定时间触发一次。
  • 使用服务端定时器。
  • 界面状态反馈。
  • 减少页面负载,减少其他脚本和样式的加载时间。

使用Web Workers

在后台线程中处理任务,以免阻塞主线程。

如何使用web worker查看另一篇文章

提高执行效率

减少业务的执行时间,从优化代码、优化算法入手。还可以采用WebAssembly编码,它可以接近原生的性能运行。

它为诸如C \ C++ \ Rust等语言提供编译目标。

可以查看文章webAssembly 学习及使用 rust

通过文章基本了解 rust 是如何编译成WebAssembly,并在浏览器中运行的。比如在之前的测试代码中使用了sort排序来加长了任务的执行时间,如果采用 rust 编译的库提供的sort函数,则可以提升好几倍的执行速度。

测试示例代码仓库 - wasm-app

相关推荐
慧一居士21 分钟前
flex 布局完整功能介绍和示例演示
前端
DoraBigHead23 分钟前
小哆啦解题记——两数失踪事件
前端·算法·面试
一斤代码6 小时前
vue3 下载图片(标签内容可转图)
前端·javascript·vue
中微子6 小时前
React Router 源码深度剖析解决面试中的深层次问题
前端·react.js
光影少年6 小时前
从前端转go开发的学习路线
前端·学习·golang
中微子6 小时前
React Router 面试指南:从基础到实战
前端·react.js·前端框架
3Katrina6 小时前
深入理解 useLayoutEffect:解决 UI "闪烁"问题的利器
前端·javascript·面试
前端_学习之路7 小时前
React--Fiber 架构
前端·react.js·架构
coderlin_7 小时前
BI布局拖拽 (1) 深入react-gird-layout源码
android·javascript·react.js
伍哥的传说8 小时前
React 实现五子棋人机对战小游戏
前端·javascript·react.js·前端框架·node.js·ecmascript·js