记录–你敢信?比 setTimeout 还快 80 倍的定时器

  • 记录–你敢信?比 setTimeout 还快 80 倍的定时器已关闭评论
  • 20 次浏览
  • A+
所属分类:Web前端
摘要

很多人都知道,setTimeout是有最小延迟时间的,根据MDN 文档 setTimeout:实际延时比设定值更久的原因:最小延迟时间中所说:


这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助

记录--你敢信?比 setTimeout 还快 80 倍的定时器

起因

很多人都知道,setTimeout是有最小延迟时间的,根据MDN 文档 setTimeout:实际延时比设定值更久的原因:最小延迟时间中所说:

在浏览器中,setTimeout()/setInterval() 的每调用一次定时器的最小间隔是 4ms,这通常是由于函数嵌套导致(嵌套层级达到一定深度)。

HTML Standard规范中也有提到更具体的:

Timers can be nested; after five such nested timers, however, the interval is forced to be at least four milliseconds.

简单来说,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 还快 80 倍的定时器

探索

假设就需要一个「立刻执行」的定时器呢?有什么办法绕过这个 4ms 的延迟吗,在 MDN 文档的角落里有一些线索:

如果想在浏览器中实现 0ms 延时的定时器,可以参考这里所说的window.postMessage()

这篇文章里的作者给出了这样一段代码,用postMessage来实现真正 0 延迟的定时器:

(function () {   var timeouts = [];   var messageName = 'zero-timeout-message';    // 保持 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类似,都属于宏任务,所以可以简单利用postMessageaddEventListener('message')的消息通知组合,来实现模拟定时器的功能。

这样,执行时机类似,但是延迟更小的定时器就完成了。

再利用上面的嵌套定时器的例子来跑一下测试:

记录--你敢信?比 setTimeout 还快 80 倍的定时器

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

测试

从理论上来说,由于postMessage的实现没有被浏览器引擎限制速度,一定是比 setTimeout 要快的。

设计一个实验方法,就是分别用postMessage版定时器和传统定时器做一个递归执行计数函数的操作,看看同样计数到 100 分别需要花多少时间。

实验代码:

function runtest() {   var output = document.getElementById('output');   var outputText = document.createTextNode('');   output.appendChild(outputText);   function printOutput(line) {     outputText.data += line + 'n';   }    var i = 0;   var startTime = Date.now();   // 通过递归 setZeroTimeout 达到 100 计数   // 达到 100 后切换成 setTimeout 来实验   function test1() {     if (++i == 100) {       var 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) {       var endTime = Date.now();       printOutput(         '100 iterations of setTimeout(0) took ' +         (endTime - startTime) +         ' milliseconds.'       );     } else {       setTimeout(test2, 0);     }   } }

实验代码很简单,先通过setZeroTimeout也就是postMessage版本来递归计数到 100,然后切换成 setTimeout计数到 100。

直接放结论,这个差距不固定,在 mac 上用无痕模式排除插件等因素的干扰后,以计数到 100 为例,大概有 80 ~ 100 倍的时间差距。在硬件更好的台式机上,甚至能到 200 倍以上。

记录--你敢信?比 setTimeout 还快 80 倍的定时器

Performance 面板

只是看冷冰冰的数字还不够过瘾,打开 Performance 面板,看看更直观的可视化界面中,postMessage版的定时器和setTimeout版的定时器是如何分布的。

记录--你敢信?比 setTimeout 还快 80 倍的定时器

这张分布图非常直观的体现出了上面所说的所有现象,左边的postMessage版本的定时器分布非常密集,大概在 5ms 以内就执行完了所有的计数任务。

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

作用

也许有同学会问,有什么场景需要无延迟的定时器?其实在 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的回调函数,来让浏览器主线程拿回控制权,进行一些更优先的渲染任务(比如用户输入)。

为什么不用执行时机更靠前的微任务呢?关键的原因在于微任务会在渲染之前执行,这样就算浏览器有紧急的渲染任务,也得等微任务执行完才能渲染。

总结

可以了解如下几个知识点:

  1. setTimeout的 4ms 延迟历史原因,具体表现。
  2. 如何通过postMessage实现一个真正 0 延迟的定时器。
  3. postMessage定时器在 React 时间切片中的运用。
  4. 为什么时间切片需要用宏任务,而不是微任务。

本文转载于:

https://juejin.cn/post/7229520942668824633

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

 记录--你敢信?比 setTimeout 还快 80 倍的定时器