网站首页 > 技术文章 正文
大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!
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 setTimeout
foo 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/
- 上一篇: 关于跨域GET、POST请求的小结
- 下一篇: Festka旗下Prefestka公路车
猜你喜欢
- 2025-05-11 四个小案例,学懂Python爬虫的requests库
- 2025-05-11 干货!最简单的检测爬虫突破封禁的方法
- 2025-05-11 SNAT/DNAT实现外网访问内网
- 2025-05-11 PHP类Web网站适应移动设备十贴士
- 2025-05-11 DrissionPage:让你的 Python 爬虫和自动化脚本飞起来!
- 2025-05-11 vue 如何实现跨域
- 2025-05-11 这个前端黑科技可能是YouTube比B站、优酷、爱奇艺加载快的原因
- 2025-05-11 实战Python爬虫(二):requests请求库
- 2025-05-11 还在用 postman?手把手教你用 curl 提高工作效率
- 2025-05-11 搭建网站SEO优化技巧:从小白到流量高手的蜕变之路
- 最近发表
- 标签列表
-
- cmd/c (64)
- c++中::是什么意思 (83)
- 标签用于 (65)
- 主键只能有一个吗 (66)
- c#console.writeline不显示 (75)
- pythoncase语句 (81)
- es6includes (73)
- sqlset (64)
- windowsscripthost (67)
- apt-getinstall-y (86)
- node_modules怎么生成 (76)
- chromepost (65)
- c++int转char (75)
- static函数和普通函数 (76)
- el-date-picker开始日期早于结束日期 (70)
- localstorage.removeitem (74)
- vector线程安全吗 (70)
- & (66)
- java (73)
- js数组插入 (83)
- linux删除一个文件夹 (65)
- mac安装java (72)
- eacces (67)
- 查看mysql是否启动 (70)
- 无效的列索引 (74)