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

瀏覽器相容性

另見