延时比指定值更长的原因

有很多因素会导致超时比设定的预期值更久,本节将讨论最常见的原因。

嵌套超时

正如 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 以零延迟来调用函数,但这个任务已经被放入了队列中并且等待下一次执行;并不是立即执行;队列中的等待函数被调用之前,当前代码必须全部运行完毕,因此这里运行结果并非预想的那样。