DC娱乐网

为何说 postMessage 才是真正的 setTimeout(0)?

大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进

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

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/