Window: setTimeout() 方法

Baseline 广泛可用 *

此特性已相当成熟,可在许多设备和浏览器版本上使用。自 ⁨2015 年 7 月⁩以来,各浏览器均已提供此特性。

* 此特性的某些部分可能存在不同级别的支持。

Window 接口的 setTimeout() 方法设置一个计时器,该计时器在计时器到期后执行一个函数或一段指定的代码。

语法

js
setTimeout(code)
setTimeout(code, delay)

setTimeout(functionRef)
setTimeout(functionRef, delay)
setTimeout(functionRef, delay, param1)
setTimeout(functionRef, delay, param1, param2)
setTimeout(functionRef, delay, param1, param2, /* …, */ paramN)

参数

functionRef

计时器到期后要执行的 function

code

一种替代语法,允许你包含一个字符串而不是函数,该字符串在计时器到期时被编译并执行。由于使用 eval() 存在安全风险,因此不推荐使用此语法。

delay 可选

计时器在执行指定函数或代码之前应等待的时间,以毫秒为单位。如果省略此参数,则使用值 0,表示“立即”执行,或者更准确地说,在下一个事件循环中执行。

请注意,在任何一种情况下,实际延迟可能比预期的要长;请参阅下面的延迟时间长于指定时间的原因

另请注意,如果该值不是数字,则会对该值进行隐式类型强制转换,以将其转换为数字——这可能导致意想不到和令人惊讶的结果;请参阅非数字延迟值被隐式强制转换为数字以获取示例。

param1, …, paramN 可选

传递给 functionRef 指定函数的其他参数。

返回值

setTimeout() 方法返回一个正整数(通常在 1 到 2,147,483,647 之间),它唯一地标识由调用创建的计时器。此标识符,通常称为“超时 ID”,可以传递给 clearTimeout() 以取消计时器。

在同一个全局环境中(例如,特定的窗口或 Worker),只要原始计时器保持活动状态,超时 ID 就保证不会用于任何新的计时器。但是,单独的全局环境维护自己的独立计时器 ID 池。

描述

超时使用 Window.clearTimeout() 取消。

要重复调用一个函数(例如,每 N 毫秒),请考虑使用 setInterval()

非数字延迟值被隐式强制转换为数字

如果 setTimeout() 使用非数字的delay 值调用,则会对该值进行隐式类型强制转换以将其转换为数字。例如,以下代码错误地将字符串 "1000" 用于 delay 值,而不是数字 1000——但它仍然有效,因为当代码运行时,该字符串被强制转换为数字 1000,因此代码在 1 秒后执行。

js
setTimeout(() => {
  console.log("Delayed for 1 second.");
}, "1000");

但在许多情况下,隐式类型强制转换可能导致意想不到和令人惊讶的结果。例如,当以下代码运行时,字符串 "1 second" 最终被强制转换为数字 0——因此,代码立即执行,没有延迟。

js
setTimeout(() => {
  console.log("Delayed for 1 second.");
}, "1 second");

因此,不要将字符串用于 delay 值,而应始终使用数字。

js
setTimeout(() => {
  console.log("Delayed for 1 second.");
}, 1000);

使用异步函数

setTimeout() 是一个异步函数,这意味着计时器函数不会暂停函数栈中其他函数的执行。换句话说,您不能使用 setTimeout() 在函数栈中下一个函数触发之前创建“暂停”。

请看以下示例

js
setTimeout(() => {
  console.log("this is the first message");
}, 5000);
setTimeout(() => {
  console.log("this is the second message");
}, 3000);
setTimeout(() => {
  console.log("this is the third message");
}, 1000);

// Output:

// this is the third message
// this is the second message
// this is the first message

请注意,第一个函数在调用第二个函数之前不会创建 5 秒的“暂停”。相反,第一个函数被调用,但等待 5 秒才执行。当第一个函数等待执行时,第二个函数被调用,并在执行之前对第二个函数应用 3 秒的等待。由于第一个和第二个函数的计时器都没有完成,第三个函数被调用并首先完成其执行。然后是第二个。最后,第一个函数在其计时器最终完成后才执行。

要创建一个只有在一个函数完成之后才触发另一个函数的进程,请参阅Promise 的文档。

“this”问题

当您将方法传递给 setTimeout() 时,它将以可能与您的预期不同的 this 值调用。一般问题在 JavaScript 参考中有详细解释。

setTimeout() 执行的代码是从与调用 setTimeout 的函数分离的执行上下文中调用的。适用于被调用函数设置 this 关键字的通常规则适用,如果您没有在调用中或使用 bind 设置 this,它将默认为 window(或 global)对象,即使在严格模式下也是如此。它将与调用 setTimeout 的函数的 this 值不同。

请看以下示例

js
const myArray = ["zero", "one", "two"];
myArray.myMethod = function (sProperty) {
  console.log(arguments.length > 0 ? this[sProperty] : this);
};

myArray.myMethod(); // prints "zero,one,two"
myArray.myMethod(1); // prints "one"

上述代码之所以有效,是因为当调用 myMethod 时,其 this 被调用设置为 myArray,因此在函数内部,this[sProperty] 等同于 myArray[sProperty]。然而,在以下代码中

js
setTimeout(myArray.myMethod, 1.0 * 1000); // prints "[object Window]" after 1 second
setTimeout(myArray.myMethod, 1.5 * 1000, "1"); // prints "undefined" after 1.5 seconds

myArray.myMethod 函数被传递给 setTimeout,然后当它被调用时,它的 this 未设置,因此它默认为 window 对象。

也没有选项像 Array 方法(例如 forEach()reduce())那样将 thisArg 传递给 setTimeout。如下所示,使用 call 设置 this 也不起作用。

js
setTimeout.call(myArray, myArray.myMethod, 2.0 * 1000); // error
setTimeout.call(myArray, myArray.myMethod, 2.5 * 1000, 2); // same error

解决方案

使用包装函数

解决此问题的一种常见方法是使用包装函数将 this 设置为所需的值。

js
setTimeout(function () {
  myArray.myMethod();
}, 2.0 * 1000); // prints "zero,one,two" after 2 seconds
setTimeout(function () {
  myArray.myMethod("1");
}, 2.5 * 1000); // prints "one" after 2.5 seconds

包装函数可以是箭头函数。

js
setTimeout(() => {
  myArray.myMethod();
}, 2.0 * 1000); // prints "zero,one,two" after 2 seconds
setTimeout(() => {
  myArray.myMethod("1");
}, 2.5 * 1000); // prints "one" after 2.5 seconds
使用 bind()

或者,您可以使用 bind() 为给定函数的所有调用设置 this 的值。

js
const myArray = ["zero", "one", "two"];
const myBoundMethod = function (sProperty) {
  console.log(arguments.length > 0 ? this[sProperty] : this);
}.bind(myArray);

myBoundMethod(); // prints "zero,one,two" because 'this' is bound to myArray in the function
myBoundMethod(1); // prints "one"
setTimeout(myBoundMethod, 1.0 * 1000); // still prints "zero,one,two" after 1 second because of the binding
setTimeout(myBoundMethod, 1.5 * 1000, "1"); // prints "one" after 1.5 seconds

传递字符串字面量

将字符串而不是函数传递给 setTimeout() 具有与使用 eval() 相同的问题。

js
// Don't do this
setTimeout("console.log('Hello World!');", 500);
js
// Do this instead
setTimeout(() => {
  console.log("Hello World!");
}, 500);

传递给 setTimeout() 的字符串是在全局上下文中评估的,因此当字符串作为代码评估时,调用 setTimeout() 的上下文中的局部符号将不可用。

延迟时间长于指定时间的原因

超时可能比预期时间长有许多原因。本节描述了最常见的原因。

嵌套超时

正如 HTML 标准中指定的那样,一旦嵌套调用 setTimeout 安排了 5 次,浏览器将强制执行 4 毫秒的最小超时时间。

这可以在以下示例中看到,其中我们嵌套了一个延迟为 0 毫秒的 setTimeout 调用,并在每次调用处理程序时记录延迟。前四次,延迟大约为 0 毫秒,之后大约为 4 毫秒。

html
<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>
js
let last = 0;
let iterations = 10;

function timeout() {
  // log the time of this call
  log(new Date().getMilliseconds());
  // if we are not finished, schedule the next call
  if (iterations-- > 0) {
    setTimeout(timeout, 0);
  }
}

function run() {
  // clear the log
  const log = document.querySelector("#log");
  while (log.lastElementChild) {
    log.removeChild(log.lastElementChild);
  }

  // initialize iteration count and the starting timestamp
  iterations = 10;
  last = new Date().getMilliseconds();
  // start timer
  setTimeout(timeout, 0);
}

function log(now) {
  // log the last timestamp, the new timestamp, and the difference
  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);

非活动选项卡中的超时

为了减少后台选项卡的负载(以及相关的电池使用),浏览器将强制执行非活动选项卡中的最小超时延迟。如果页面使用 Web Audio API AudioContext 播放声音,也可能会取消。

具体情况因浏览器而异。

  • Firefox Desktop 对非活动选项卡的最小超时时间为 1 秒。

  • Firefox for Android 对非活动选项卡的最小超时时间为 15 分钟,并且可能完全卸载它们。

  • 如果选项卡包含 AudioContext,Firefox 不会限制非活动选项卡。

  • Chrome 根据选项卡活动使用不同级别的限制。

    • 最小限制:适用于当页面可见、最近发出声音或被 Chrome 视为活动状态时的计时器。计时器接近请求的间隔运行。

    • 限制:适用于当不满足最小限制条件并且以下任何条件为真时的计时器

      • 嵌套计数(即,链式计时器调用的数量)小于 5。
      • 页面不可见时间少于 5 分钟。
      • WebRTC 处于活动状态。

    此状态下的计时器每秒检查一次,可以与其他具有类似超时的计时器批量处理。

    • 强化限制:在 Chrome 88(2021 年 1 月)中引入。适用于当既不满足最小限制也不满足限制条件,并且满足以下所有条件时的计时器
      • 嵌套计数为 5 或更高。
      • 页面不可见时间超过 5 分钟。
      • 页面静默时间超过 30 秒。
      • WebRTC 不活动。

    此状态下的计时器每分钟检查一次,可以与其他具有类似超时的计时器批量处理。

跟踪脚本的限制

Firefox 对其识别为跟踪脚本的脚本强制执行额外的限制。在前台运行时,限制最小延迟仍为 4ms。然而,在后台选项卡中,限制最小延迟为 10,000 毫秒,即 10 秒,这在文档首次加载后 30 秒生效。

有关更多详细信息,请参阅 跟踪保护

延迟超时

如果页面(或操作系统/浏览器)忙于其他任务,超时也可能比预期时间晚。需要注意的一个重要情况是,在调用 setTimeout() 的线程终止之前,无法执行函数或代码片段。例如

js
function foo() {
  console.log("foo has been called");
}
setTimeout(foo, 0);
console.log("After setTimeout");

将写入控制台

After setTimeout
foo has been called

这是因为即使 setTimeout 以零延迟调用,它也被放置在队列中并安排在下一个机会运行;而不是立即运行。当前正在执行的代码必须在队列中的函数执行之前完成,因此结果执行顺序可能与预期不同。

页面加载期间的超时延迟

在当前选项卡加载期间,Firefox 将推迟触发 setTimeout() 计时器。触发会延迟,直到主线程被视为空闲(类似于 Window.requestIdleCallback()),或者直到加载事件触发。

WebExtension 后台页面和计时器

WebExtensions 中,setTimeout() 不可靠。扩展作者应改为使用 alarms API。

最大延迟值

浏览器在内部将延迟存储为 32 位有符号整数。当使用大于 2,147,483,647 毫秒(约 24.8 天)的延迟时,会导致整数溢出。例如,此代码

js
setTimeout(() => console.log("hi!"), 2 ** 32 - 5000);

...导致超时立即执行(因为 2**32 - 5000 溢出为负数),而以下代码

js
setTimeout(() => console.log("hi!"), 2 ** 32 + 5000);

...导致超时在大约 5 秒后执行。

注意:这与 Node.js 中的 setTimeout 行为不符,在 Node.js 中,任何大于 2,147,483,647 毫秒的超时都会导致立即执行。

示例

设置和清除超时

以下示例在网页中设置了两个简单的按钮,并将它们连接到 setTimeout()clearTimeout() 例程。按下第一个按钮将设置一个超时,该超时在两秒后显示一条消息,并存储超时 ID 以供 clearTimeout() 使用。您可以选择通过按下第二个按钮来取消此超时。

HTML

html
<button id="show">Show a message after two seconds</button>
<button id="cancel">Cancel message before it happens</button>

<div id="output"></div>

JavaScript

js
let timeoutID;

function setOutput(outputContent) {
  document.querySelector("#output").textContent = outputContent;
}

function delayedMessage() {
  setOutput("");
  timeoutID = setTimeout(setOutput, 2 * 1000, "That was really slow!");
}

function clearMessage() {
  clearTimeout(timeoutID);
}

document.getElementById("show").addEventListener("click", delayedMessage);
document.getElementById("cancel").addEventListener("click", clearMessage);

结果

另请参阅 clearTimeout() 示例。

规范

规范
HTML
# dom-settimeout-dev

浏览器兼容性

另见