延时比指定值更长的原因
有很多因素会导致超时比设定的预期值更久,本节将讨论最常见的原因。
嵌套超时
正如 HTML 标准中规定的那样,一旦对 setTimeout 的嵌套调用被安排了 5 次,浏览器将强制执行 4 毫秒的最小超时。
这可以在下面的例子中看到,在这个例子中,我们嵌套了对 setTimeout 的调用,延迟为 0 毫秒,并记录每次调用处理程序时的延迟。前四次,延迟约为 0 毫秒,之后约为 4 毫秒:
html
| 之前 | 现在 | 实际延时 |
|---|
jslet 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);
* {
font-family: monospace;
}
th,
td {
padding: 0 10px 0 10px;
text-align: center;
border: 1px solid;
}
table {
border-collapse: collapse;
margin-top: 10px;
}
非活动标签的超时
为了优化后台标签的加载损耗(以及降低耗电量),浏览器会在非活动标签中强制执行一个最小的超时延迟。如果一个页面正在使用 Web 音频 API AudioContext 播放声音,也可以不执行该延迟。
这方面的具体情况与浏览器有关:
Firefox 桌面版和 Chrome 针对不活动标签都有一个 1 秒的最小超时值。
安卓版 Firefox 浏览器对不活动的标签有一个至少 15 分钟的超时,并可能完全卸载它们。
如果标签中包含 AudioContext,Firefox 不会对非活动标签进行限流。
追踪型脚本的限流
Firefox 对它识别为追踪型脚本的脚本实施额外的限流。当在前台运行时,限流的最小延迟仍然是 4 毫秒。然而,在后台标签中,限流的最小延迟是 10000 毫秒(即 10 秒),在文档首次加载后 30 秒开始生效。
参见跟踪保护以了解更多信息。
超时延迟
如果页面(或操作系统/浏览器)正忙于其他任务,超时也可能比预期的晚。需要注意的一个重要情况是,在调用 setTimeout() 的线程结束之前,函数或代码片段不能被执行。例如:
jsfunction foo() {
console.log("foo 被调用");
}
setTimeout(foo, 0);
console.log("setTimeout 之后");
会在控制台输出:
setTimeout 之后
foo 被调用
出现这个结果的原因是,尽管 setTimeout 以零延迟来调用函数,但这个任务已经被放入了队列中并且等待下一次执行;并不是立即执行;队列中的等待函数被调用之前,当前代码必须全部运行完毕,因此这里运行结果并非预想的那样。