首页 > 其他分享 >聊聊定时器 setTimeout 的时延问题

聊聊定时器 setTimeout 的时延问题

时间:2023-11-14 15:07:25浏览次数:26  
标签:now 定时器 4ms let 时延 setTimeout 延迟


全局的 setTimeout()  方法设置一个定时器,一旦定时器到期,就会执行一个函数或指定的代码片段,但是需要注意的是,setTimeout 并不是 ECMAScript 标准的一部分,不过几乎每一个 JS 运行时都支持了这个函数。定时器的使用比较简单,这里不再阐述,我们这篇文章主要聊下关于 setTimeout 有最小时延的相关知识。

一、为什么定时器有最小时延

其实 setTimeout 是有最小时延的,这是为什么呢?有很多因素会导致 setTimeout 的回调函数执行比设定的预期值更久,这里列举一些导致定时器出现时延的原因:

1. 函数过度嵌套

MDN 文档:在浏览器中,每调用一次定时器的最小间隔是 4ms,这通常是由于函数嵌套导致(嵌套层级达到一定深度),5 层以上的定时器嵌套会导致至少 4ms 的延迟。

let a = performance.now();
setTimeout(() => {
  let b = performance.now();
  console.log(b - a);
  setTimeout(() => {
    let c = performance.now();
    console.log(c - b);
    setTimeout(() => {
      let d = performance.now();
      console.log(d - c);
      setTimeout(() => {
        let e = performance.now();
        console.log(e - d);
        setTimeout(() => {
          let f = performance.now();
          console.log(f - e);
          setTimeout(() => {
            let g = performance.now();
            console.log(g - f);
          }, 0);
        }, 0);
      }, 0);
    }, 0);
  }, 0);
}, 0);

在浏览器中的打印结果大概是这样的,和规范一致,第五次执行的时候延迟来到了 4ms 以上:

聊聊定时器 setTimeout 的时延问题_嵌套

2. 非活动标签的超时

为了优化后台标签的加载损耗(以及降低耗电量),浏览器会在非活动标签中强制执行一个最小的超时延迟,如果一个页面正在使用网络音频 API 播放声音,也可以不执行该延迟。

这方面的具体情况与浏览器有关:

  • Firefox 桌面版和 Chrome 针对不活动标签都有一个 1 秒的最小超时值
  • 安卓版 Firefox 浏览器对不活动的标签有一个至少 15 分钟的超时,并可能完全卸载它们

3. 追踪型脚本的节流

Firefox 对它识别为追踪型脚本的脚本实施额外的节流,当在前台运行时,节流的最小延迟仍然是 4ms,然而,在后台标签中,节流的最小延迟是 10000 毫秒,即 10 秒,在文档首次加载后 30 秒开始生效。

4. 超时延迟

如果页面(或操作系统/浏览器)正忙于其他任务,超时也可能比预期的晚,需要注意的一个重要情况是,在调用 setTimeout() 的线程结束之前,函数或代码片段不能被执行。例如:

function foo() {
  console.log("foo 被调用");
}
setTimeout(foo, 0);
console.log("setTimeout 之后");

// 控制台输出:
// setTimeout 之后
// foo 被调用

出现这个结果的原因是,尽管 setTimeout 以 0ms 的延迟来调用函数,但这个任务已经被放入了队列中并且等待下一次执行,并不是立即执行;队列中的等待函数被调用之前,当前代码必须全部运行完毕,因此这里运行结果并非预想的那样。

5. 在加载页面时推迟超时

当前标签页正在加载时,Firefox 将推迟触发 setTimeout() 计时器,直到主线程被认为是空闲的,类似于 window.requestIdleCallback(),或者直到加载事件触发完毕,才开始触发。

二、为什么定时器最小时延是4ms

我们首先要知道是不是存在具体的规范来指定了 4ms, 还是只是业界实践的既定事实?

1. HTML standard

在 HTML standard 8.6 Timers-2020/6/23 中对于 setTimeout() 有详细的描述,我们只看其中的 10-13 行:

  1. If timeout is less than 0, then set timeout to 0.
  2. If nesting level is greater than 5, and timeout is less than 4, then set timeout to 4.
  3. Increment nesting level by one.
  4. Let task's timer nesting level be nesting level.

从上面的规范可以看出来:

  1. 如果设置的 timeout 小于 0,则设置为 0
  2. 如果嵌套的层级超过了 5 层,并且 timeout 小于 4ms,则设置 timeout 为 4ms

到这里,我们似乎已经找到了 4ms 的出处,并且对于 setTimeout 的最小延迟有了更加精确的定义 - 需要同时满足嵌套层级超过 5 层,timeout 小于 4ms,才会设置 4ms

2. 浏览器源码

除了寻找规范的出处之外,还可以去浏览器的源码中寻找答案,我们进入到谷歌浏览器源码中来查找用来设置计时器延时的地方:

static const int maxIntervalForUserGestureForwarding = 1000; // One second matches Gecko.
static const int maxTimerNestingLevel = 5;
static const double oneMillisecond = 0.001;
// Chromium uses a minimum timer interval of 4ms. We'd like to go
// lower; however, there are poorly coded websites out there which do
// create CPU-spinning loops.  Using 4ms prevents the CPU from
// spinning too busily and provides a balance between CPU spinning and
// the smallest possible interval timer.
static const double minimumInterval = 0.004;
double intervalMilliseconds = std::max(oneMillisecond, interval * oneMillisecond);
if (intervalMilliseconds < minimumInterval && m_nestingLevel >= maxTimerNestingLevel)
    intervalMilliseconds = minimumInterval;

代码逻辑很清晰,设置了几个常量:

  1. maxTimerNestingLevel = 5,也就是 HTML standard 当中提到的嵌套层级
  2. minimumInterval = 0.004,也就是 HTML standard 当中说的最小延迟

在第二段代码中我们会看到,首先会在 延迟时间 和 1ms 之间取一个最大值,换句话说,在不满足嵌套层级的情况下,最小延迟时间设置为 1ms,这也解释了为什么在 chrome 中测试 setTimeout 是上面的结果。

在 chromium 的注释中,解释了为什么要设置 minimumInterval = 4ms。简单来讲,本身 chromium 团队想要设置更低的延迟时间(其实他们期望达到亚毫秒级别),但是由于某些网站对 setTimeout 这种计时器不良的使用,设置延迟过低会导致 CPU-spinning,因此 chromium 做了些测试,选定了 4ms 作为其 minimumInterval。

到这里为止,从浏览器厂商角度和 HTML standard 规范角度都解释了 4ms 的来源和其更加精确的定义。

三、如何实现立刻执行的定时器

假设我们就需要一个「立刻执行」的定时器呢?有什么办法绕过这个 4ms 的延迟吗,其实可以采用 postMessage 来实现真正 0 延迟的定时器:

(function () {
  var timeouts = [];
  var messageName = 'message-zeroTimeout';

  // 保持 setTimeout 的形态,只接受单个函数的参数,延迟始终为 0。
  function setZeroTimeout(fn) {
    timeouts.push(fn);
    window.postMessage(messageName, '*');
  }

  function handleMessage(event) {
    if (event.source == window && event.data == messageName) {
      event.stopPropagation();
      if (timeouts.length > 0) {
        var fn = timeouts.shift();
        fn();
      }
    }
  }

  window.addEventListener('message', handleMessage, true);

  // 把 API 添加到 window 对象上
  window.setZeroTimeout = setZeroTimeout;
})();

由于 postMessage 的回调函数的执行时机和 setTimeout 类似,都属于宏任务,所以可以简单利用 postMessage 和 addEventListener('message') 的消息通知组合,来实现模拟定时器的功能,再利用上面的嵌套定时器的例子来跑一下测试:

聊聊定时器 setTimeout 的时延问题_加载_02

全部在 0.1 ~ 0.3 毫秒级别,而且不会随着嵌套层数的增多而增加延迟

1. 测试验证

从理论上来说,由于 postMessage 的实现没有被浏览器引擎限制速度,一定是比 setTimeout 要快的,这里用数据进行验证:分别用 postMessage 版定时器和传统定时器做一个递归执行计数函数的操作,看看同样计数到 100 分别需要花多少时间

function runtest() {
  let output = document.getElementById('output');
  let outputText = document.createTextNode('');
  output.appendChild(outputText);
  function printOutput(line) {
    outputText.data += line + '\n';
  }

  let i = 0;
  let startTime = Date.now();
  // 通过递归 setZeroTimeout 达到 100 计数
  // 达到 100 后切换成 setTimeout 来实验
  function test1() {
    if (++i == 100) {
      let endTime = Date.now();
      printOutput(
        '100 iterations of setZeroTimeout took ' +
          (endTime - startTime) +
          ' milliseconds.'
      );
      i = 0;
      startTime = Date.now();
      setTimeout(test2, 0);
    } else {
      setZeroTimeout(test1);
    }
  }

  setZeroTimeout(test1);

  // 通过递归 setTimeout 达到 100 计数
  function test2() {
    if (++i == 100) {
      let endTime = Date.now();
      printOutput(
        '100 iterations of setTimeout(0) took ' +
          (endTime - startTime) +
          ' milliseconds.'
      );
    } else {
      setTimeout(test2, 0);
    }
  }
}

结论如下:

聊聊定时器 setTimeout 的时延问题_开发语言_03

2. Performance 面板分析

我们打开 Performance 面板,看看更直观的可视化界面中,两个版本的定时器是如何分布的:

聊聊定时器 setTimeout 的时延问题_开发语言_04

左边的 postMessage 版本的定时器分布非常密集,大概在 5ms 以内就执行完了所有的计数任务

而右边的 setTimeout 版本相比较下分布的就很稀疏了,而且通过上方的时间轴可以看出,前四次的执行间隔大概在 1ms 左右,到了第五次就拉开到 4ms 以上

3. 无延迟的定时器的作用

你可能会有疑问,什么应用场景下需要用到无延迟的定时器?其实在 React 的源码中,做时间切片的部分就用到了:

const channel = new MessageChannel();
const port = channel.port2;

// 每次 port.postMessage() 调用就会添加一个宏任务
// 该宏任务为调用 scheduler.scheduleTask 方法
channel.port1.onmessage = scheduler.scheduleTask;

const scheduler = {
  scheduleTask() {
    // 挑选一个任务并执行
    const task = pickTask();
    const continuousTask = task();

    // 如果当前任务未完成,则在下个宏任务继续执行
    if (continuousTask) {
      port.postMessage(null);
    }
  },
};

React 把任务切分成很多片段,这样就可以通过把任务交给 postMessage 的回调函数,来让浏览器主线程拿回控制权,进行一些更优先的渲染任务。

标签:now,定时器,4ms,let,时延,setTimeout,延迟
From: https://blog.51cto.com/u_14627797/8369383

相关文章

  • go使用定时器
    go使用定时器packagemainimport( "fmt" "os" "os/signal" "syscall" "time")funcmain(){ initTask()}funcstop(ticker*time.Ticker){ //创建一个通道来接收信号 sigCh:=make(chanos.Signal,1) //监听指定的信号 si......
  • Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmedia
    下列关于Vue的描述错误的是()A当给某个组件修改某个值时,该组件不会立即重新渲染BVue内部使用原生Promise.then、MutationObserver和setImmediate实现异步队列,不会采用setTimeout(fn,0)C$nextTick()返回一个Promise对象D$nextTick()可以配合async/await使用正确答案:B官......
  • Timer是jdk中提供的一个定时器工具,TImer用法
    说到任务调度,大家可能会想到Quartz框架,但是jdk自带的简单任务调度工具类,反而了解的人并不是很多。我觉得如果你的业务相对简单的话,没必要非得用Quartz等框架,使用Timer完全可以胜任的。简单来分享一下我了解的Timer。Timer是jdk中提供的一个定时器工具,使用的时候会在主线程之外起......
  • JavaScript--定时器案例
    <!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width,initial-scale=1.0"><title>Document</title><......
  • 11_定时器
    定时器定时器介绍STC89C52定时器资源定时器框图定时器工作模式定时器时钟中断系统中断程序流程STC89C52中断资源定时器和中断系统定时器相关寄存器使用定时器0每隔一秒使LED亮灭Timer0.c#include<REGX52.H>/***@brief定时器0初始化,1毫秒@12.000MHz......
  • DolphinDB 流计算优化实践:时延统计与性能调优
    在实时计算中,端到端的响应延迟是衡量计算性能时最重要的指标。DolphinDB内置的流数据框架支持流数据的发布与订阅、流式增量计算、实时关联等,用户能够快速实现复杂的实时计算任务,达到毫秒级甚至亚毫秒级的效果,而无需编写大量代码。本文介绍如何对DolphinDB流计算任务进行全链路......
  • setTimeout 是 DOM 提供的函数,不是JavaScript的全局函数
    JavaScript中包含以下7个全局函数,用于完成一些常用的功能(以后的章节中可能会用到):escape()、unescape()、eval()、isFinite()、isNaN()、parseFloat()、parseInt()函数描述decodeURI()解码某个编码的URI。decodeURIComponent()解码一个编码的URI组件。......
  • 51单片机定时器和中断知识
    什么是定时器/计数器?---既可以定时,也能计数的一种单片机内部资源,主要用于软件计时和软件延时等场合。但同一时刻只能使用其中一种功能。定时器的核心功能简单一句话说就是“启动定时器后,每个机器周期到来,初值寄存器自动加1,直到计满溢出”。定时器的工作流程:1.启动信号2.预设机器......
  • 24. 从零用Rust编写正反向代理,细说HTTP行为中的几种定时器
    wmproxywmproxy已用Rust实现http/https代理,socks5代理,反向代理,静态文件服务器,四层TCP/UDP转发,内网穿透,后续将实现websocket代理等,会将实现过程分享出来,感兴趣的可以一起造个轮子项目地址国内:https://gitee.com/tickbh/wmproxygithub:https://github.com/tickbh/wmpro......
  • Jmeter-定时器
    同步定时器在性能测试中,尤其是抢购/抢红包/秒杀活动业务等某时段集中访问场景,需要模拟多用户集中并发测试,此时需要用到同步定时器(SynchronizingTimer)。每次SyncTimer的目的是阻塞线程,直到阻塞了n个线程,然后立即释放它们。模拟用户数:当设置为0时,等同于线程组中设置的用户数量,......