大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!

setTimeout 通常用于将任务分成更小的部分,以便在执行任务时用户能与页面正常交互,同时在一定的时间后还能继续之前的工作。
然而,不幸的是,大多数浏览器中的 setTimeout 并非真正的 0 毫秒延迟。因此,从这个时间来看,任务并没有尽快完成! 虽然,Chrome 已将其更改为 2 毫秒,但依然存在一些问题。
1.一起看看 setTimeout(0)延迟代码下面的代码演示了 setTimeout 延迟,即以 0 延迟递归调用 setTimeout 100 次。
正如亲眼所见,运行 100 次迭代的执行时间约为 500 毫秒,您也可以重新执行该代码片段
function bar(iterations) { if (iterations === 0) { console.log('done in: ' + (new Date() - startTimeout) + ' msec'); } else { setTimeout(bar, 0, iterations - 1); }}startTimeout = new Date();console.log('Start1');bar(100);2.为什么 setTimeout 时钟不准在现代浏览器中,setTimeout 被限制在 4ms 左右,因此协程之间的通信以及挂起、恢复的进度将非常缓慢,因为协程恢复当前是使用零内部 setTimeout 进行调度的。
有多种原因导致 setTimeout 执行可能需要比预期更长的时间。
2.1 嵌套超时根据 HTML 标准中的规定,一旦对 setTimeout 的嵌套调用被安排了 5 次,浏览器将强制执行 4 毫秒的最小超时。
下面的示例嵌套了对 setTimeout 的调用,延迟为 0 毫秒,并在每次调用处理程序时记录延迟。 前四次延迟约为 0 毫秒,之后约为 4 毫秒:
<button id="run">Run</button><table> <thead> <tr> <th>Previous</th> <th>This</th> <th>Actual delay</th> </tr> </thead> <tbody id="log"></tbody></table>下面是 JavaScript 代码:
let last = 0;let iterations = 10;function timeout() { // 打印本地调用时间 logline(new Date().getMilliseconds()); if (iterations-- > 0) { setTimeout(timeout, 0); }}function run() { const log = document.querySelector('#log'); while (log.lastElementChild) { log.removeChild(log.lastElementChild); } iterations = 10; last = new Date().getMilliseconds(); setTimeout(timeout, 0);}function logline(now) { const tableBody = document.getElementById('log'); const logRow = tableBody.insertRow(); logRow.insertCell().textContent = last; logRow.insertCell().textContent = now; logRow.insertCell().textContent = now - last; last = now;}document.querySelector('#run').addEventListener('click', run);运行以上代码多次,真实延迟数据如下:
2.2 非活动 Tab 超时为了减少后台选项卡的负载(以及相关的电池使用量),浏览器将在非活动选项卡中强制执行最小超时延迟。 如果页面使用 Web Audio API AudioContext 播放声音,也可能会被放弃。
具体细节取决于浏览器:
Firefox Desktop 和 Chrome 的非活动选项卡的最小超时时间均为 1 秒。Android 版 Firefox 对于非活动选项卡的最短超时时间为 15 分钟,并且可能会完全卸载。如果选项卡包含 AudioContext,则 Firefox 不会限制非活动选项卡。2.3 跟踪脚本的限制Firefox 对识别为跟踪脚本的脚本实施额外的限制。 前台运行时,节流最小延迟仍为 4ms。 然而,在后台选项卡中,限制最小延迟为 10,000 毫秒,即 10 秒,在文档首次加载后 30 秒生效。
2.4 延迟的 setTimeout如果页面(或操作系统/浏览器)正忙于其他任务,则 setTimeout 也可能比预期晚触发。 需要注意的一个点是,在调用 setTimeout() 的线程终止之前,setTimeout 函数或代码片段无法执行。 例如:
function foo() { console.log('foo has been called');}setTimeout(foo, 0);console.log('After setTimeout');控制台将打印:
After setTimeoutfoo has been called这是因为即使 setTimeout 的延迟为零,也会被放置在队列中并计划在下一个循环运行而不是立即。 当前正在执行的代码必须在队列上的函数执行之前完成,因此结果执行顺序可能不符合预期。
2.5 页面加载期间延迟超时Firefox 将在当前选项卡加载时推迟触发 setTimeout() 计时器。 触发会被推迟,直到主线程被视为空闲(类似于 window.requestIdleCallback()),或者直到触发 load 事件。
2.6 WebExtension 后台页面和计时器在 WebExtensions 中,setTimeout() 无法可靠地工作。 扩展作者应该使用警报 API。
2.7 最大延迟值浏览器在内部将延迟存储为 32 位有符号整数。 当使用大于 2,147,483,647 毫秒(约 24.8 天)的延迟时,会导致整数溢出,导致超时立即执行。
3.使用 postMessage 无限接近真正零延迟下面的方法使用 postMessage 获得相当于 setTimeout(0) 的效果,实现真正的零延迟。
// 将setZeroTimeout添加到window上,其他通过闭包抹平副作用(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); // 添加事件监听器到Window对象 window.setZeroTimeout = setZeroTimeout;})();实际运行场景,setZeroTimeout 证明比 setTimeout(0) 快得多。 在 Firefox nightly 上,setZeroTimeout 的 100 次迭代的大部分约需要 10-20 毫秒,但有时会更长; 在 WebKit 构建上,需要大约 4-6 毫秒,但有时也会更长一些。
相比之下,在 Firefox 和非基于 Chromium 的 WebKit 上,setTimeout 版本大约需要一秒钟,而在 Windows 上可能更长。
以下是完整的测试代码:
(function () { var timeouts = []; var messageName = 'zero-timeout-message'; 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); window.setZeroTimeout = setZeroTimeout;})();// 下面使用setZeroTimeout方法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(); 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); //执行100次,即100次setTimeout和100次setZeroTimeout function test2() { if (++i == 100) { var endTime = Date.now(); printOutput( '100 iterations of setTimeout(0) took ' + (endTime - startTime) + ' milliseconds.' ); } else { setTimeout(test2, 0); } }}参考资料https://dbaron.org/log/20100309-faster-timeouts
https://github.com/Kotlin/kotlinx.coroutines/issues/194
https://bugs.chromium.org/p/chromium/issues/detail?id=888
https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#reasons_for_delays_longer_than_specified
https://blog.klipse.tech/javascript/2016/11/01/setTimeout-0msec.html
https://stackoverflow.com/questions/779379/why-is-settimeoutfn-0-sometimes-useful
https://dmitripavlutin.com/javascript-promises-settimeout/
https://www.educba.com/javascript-settimeout/