【每日一面】setTimeout 延時為 0 的情況
基礎問答
問題:你在寫代碼的過程中,在什么時候才會設置 setTimeout 的延時為 0?
回答:有如下幾種情況
- 避免同步任務阻塞 UI,即在渲染較多數據的時候,可以通過 setTimeout 分批渲染。
const data = new Array(1000).fill(1).map((x, idx) => idx + 1);
function render(list) {
let index = 0;
for (; index < list.length; index += 100) {
console.log('current', index);
const current = index;
setTimeout(() => {
console.log(list.slice(current, current + 100).join(','))
}, 0);
}
}
render(data);
- 獲取 DOM 元素的寬高,本質是根據事件循環機制調整了代碼的執行順序。
function App() {
const dom = document.querySelector('#app');
console.log(dom.height);
setTimeout(() => dom.height, 0);
}
- 代碼分片,古早技術,將同步代碼分片執行,避免阻塞渲染。
擴展延伸
JavaScript 單線程:JavaScript 是單線程語言,這個是編程語言的設計,在同一時間只能執行一段代碼,所有的任務都需要排隊,而身為單線程,但是好像我們訪問網頁的時候還是那么快,這語言優勢這么強?這是另一個問題,語言設計上是單線程,只能同步的執行代碼,但是瀏覽器不是,他是多線程的,分出來一個 JS 主線程用于執行 JavaScript 代碼,還有如 UI 線程,用于執行渲染等。在 JavaScript 中,通過事件循環來協調任務執行,實現異步編程。
事件循環:這個機制是 JavaScript 的一個核心機制,可以利用這個機制實現高并發,異步編程操作。
核心是 - 調用棧、任務隊列、宏任務、微任務。
整個流程為 - JavaScript 代碼按照代碼依次執行時,檢測到同步任務就進入調用棧執行,檢測到宏任務,先壓入宏任務隊列,檢測到微任務,則壓入微任務隊列,當本輪同步任務(宏任務)結束時,檢測微任務隊列,清空(即執行所有的微任務),這個檢測的時機稱為“微任務檢查點”。

如圖,伴隨著每個宏任務執行,都有自己對應的微任務隊列,直到微任務隊列全部執行完成,才會開啟下一個宏任務。
setTimeout(callback, delayTime) API:在執行這個 API 時,JS 引擎會將 callback 函數封裝成宏任務,掛載到延遲隊列中,等待執行。這里再次引入了一個新的概念,延遲隊列,這個是瀏覽器(或者引擎)實現的,當 JavaScript 創建定時器的時候,渲染進程就會將這個定時器的任務添加到延遲隊列中。執行完一個任務,計算延遲隊列中是否有到期的任務,有就執行,沒有繼續循環。
面試追問
- 延遲時間為 0,會立即執行嗎?
不會,雖然我們設置為了 0,但是 setTimeout 的回調函數會被封裝成一個宏任務,所以他需要等待同步任務執行結束后,從宏任務隊列中取出來執行。此外,這個延遲時間雖然可以設置為 0,但是瀏覽器的最小執行時間實際是不一定的,Chrome 瀏覽器是 4ms。
- 那延遲時間設置為 400ms,會在 400ms 時執行嗎?
不會,原因同上。setTimeout 只能做到“盡快執行”,而不是“立即執行”。
- 你在使用
setTimeout的時候,有遇到過什么問題嗎?
歷史代碼問題,存在比較多的 setTimeout 導致代碼執行的結果不好理解。
this 指針問題,setTimeout 回調函數中的 this 和直覺不符,如果執行的回調函數是一個對象的方法,那么這個對象的方法中 this 并不是指向這個對象,而是全局。
長任務阻塞延遲的回調函數調用,如果當前任務執行的時間比較長,可能會導致回調函數等待。
瀏覽器優化問題,現在瀏覽器為了降低對電量的消耗,延長續航時間,會對后臺界面的 setTimeout 執行時間間隔延長,一般會大于 1s,但是遇到過更久的,有一個多小時。
- 那有沒有可以替代的 API?
有,和動畫相關的可以使用 requestAnimationFrame API 來替代,可以保持和瀏覽器渲染頻率一致,而不需要計算每幀的間隔時間來延遲執行。
微任務可以使用 Promise 來創建。
- 實現一個簡單的 setTimeout。
/**
* 用 requestAnimationFrame 實現簡易 setTimeout
* @param {number} delay - 延遲時間(毫秒)
* @returns {number} - RAF的ID,用于取消(對應clearTimeout)
*/?
function rafSetTimeout(callback, delay) {
// 1. 記錄延遲結束的目標時間(當前時間 + 延遲時間)
const startTime = Date.now();
const targetTime = startTime + delay;
// 2. 定義遞歸執行的RAF回調函數
function rafCallback() {
// 3. 檢查當前時間是否達到目標時間
if (Date.now() >= targetTime) {
// 達到目標時間,執行用戶回調
callback();
} else {
// 未達到,繼續遞歸調用RAF,等待下一次重繪
requestAnimationFrame(rafCallback);
}
}
// 4. 啟動第一次RAF,開始等待
return requestAnimationFrame(rafCallback);
}
/**
* 對應 clearTimeout,取消 rafSetTimeout
* @param {number} rafId - rafSetTimeout 返回的RAF ID
*/
function rafClearTimeout(rafId) {
cancelAnimationFrame(rafId);
}
- 經典題目,判斷運行結果,這里給個簡單的例子。
setTimeout(() => {
console.log('回調1');
}, 0);
// 插入同步任務
console.log('同步任務');
setTimeout(() => {
console.log('回調2');
}, 0);


浙公網安備 33010602011771號