這里給大家分享我在網(wǎng)上總結(jié)出來(lái)的一些知識(shí),希望對(duì)大家有所幫助
起因
很多人都知道,setTimeout
是有最小延遲時(shí)間的,根據(jù)MDN 文檔 setTimeout:實(shí)際延時(shí)比設(shè)定值更久的原因:最小延遲時(shí)間中所說(shuō):
在瀏覽器中,
setTimeout()
/setInterval()
的每調(diào)用一次定時(shí)器的最小間隔是 4ms,這通常是由于函數(shù)嵌套導(dǎo)致(嵌套層級(jí)達(dá)到一定深度)。
在HTML Standard規(guī)范中也有提到更具體的:
Timers can be nested; after five such nested timers, however, the interval is forced to be at least four milliseconds.
簡(jiǎn)單來(lái)說(shuō),5 層以上的定時(shí)器嵌套會(huì)導(dǎo)致至少 4ms 的延遲。
用如下代碼做個(gè)測(cè)試:
let a = performance.now(); setTimeout(() => { let b = performance.now(); console.log(b - a); setTimeout(() => { let c = performance.now(); console.log(c - b); setTimeout(() => { let d = performance.now(); console.log(d - c); setTimeout(() => { let e = performance.now(); console.log(e - d); setTimeout(() => { let f = performance.now(); console.log(f - e); setTimeout(() => { let g = performance.now(); console.log(g - f); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0);
在瀏覽器中的打印結(jié)果大概是這樣的,和規(guī)范一致,第五次執(zhí)行的時(shí)候延遲來(lái)到了 4ms 以上。
探索
假設(shè)就需要一個(gè)「立刻執(zhí)行」的定時(shí)器呢?有什么辦法繞過(guò)這個(gè) 4ms 的延遲嗎,在 MDN 文檔的角落里有一些線索:
如果想在瀏覽器中實(shí)現(xiàn) 0ms 延時(shí)的定時(shí)器,可以參考這里所說(shuō)的
window.postMessage()
。
這篇文章里的作者給出了這樣一段代碼,用postMessage
來(lái)實(shí)現(xiàn)真正 0 延遲的定時(shí)器:
(function () { var timeouts = []; var messageName = 'zero-timeout-message'; // 保持 setTimeout 的形態(tài),只接受單個(gè)函數(shù)的參數(shù),延遲始終為 0。 function setZeroTimeout(fn) { timeouts.push(fn); window.postMessage(messageName, '*'); } function handleMessage(event) { if (event.source == window && event.data == messageName) { event.stopPropagation(); if (timeouts.length > 0) { var fn = timeouts.shift(); fn(); } } } window.addEventListener('message', handleMessage, true); // 把 API 添加到 window 對(duì)象上 window.setZeroTimeout = setZeroTimeout; })();
由于postMessage
的回調(diào)函數(shù)的執(zhí)行時(shí)機(jī)和setTimeout
類似,都屬于宏任務(wù),所以可以簡(jiǎn)單利用postMessage
和addEventListener('message')
的消息通知組合,來(lái)實(shí)現(xiàn)模擬定時(shí)器的功能。
這樣,執(zhí)行時(shí)機(jī)類似,但是延遲更小的定時(shí)器就完成了。
再利用上面的嵌套定時(shí)器的例子來(lái)跑一下測(cè)試:
全部在 0.1 ~ 0.3 毫秒級(jí)別,而且不會(huì)隨著嵌套層數(shù)的增多而增加延遲。
測(cè)試
從理論上來(lái)說(shuō),由于postMessage
的實(shí)現(xiàn)沒(méi)有被瀏覽器引擎限制速度,一定是比 setTimeout
要快的。
設(shè)計(jì)一個(gè)實(shí)驗(yàn)方法,就是分別用postMessage
版定時(shí)器和傳統(tǒng)定時(shí)器做一個(gè)遞歸執(zhí)行計(jì)數(shù)函數(shù)的操作,看看同樣計(jì)數(shù)到 100 分別需要花多少時(shí)間。
實(shí)驗(yàn)代碼:
function runtest() { var output = document.getElementById('output'); var outputText = document.createTextNode(''); output.appendChild(outputText); function printOutput(line) { outputText.data += line + '\n'; } var i = 0; var startTime = Date.now(); // 通過(guò)遞歸 setZeroTimeout 達(dá)到 100 計(jì)數(shù) // 達(dá)到 100 后切換成 setTimeout 來(lái)實(shí)驗(yàn) function test1() { if (++i == 100) { var endTime = Date.now(); printOutput( '100 iterations of setZeroTimeout took ' + (endTime - startTime) + ' milliseconds.' ); i = 0; startTime = Date.now(); setTimeout(test2, 0); } else { setZeroTimeout(test1); } } setZeroTimeout(test1); // 通過(guò)遞歸 setTimeout 達(dá)到 100 計(jì)數(shù) function test2() { if (++i == 100) { var endTime = Date.now(); printOutput( '100 iterations of setTimeout(0) took ' + (endTime - startTime) + ' milliseconds.' ); } else { setTimeout(test2, 0); } } }
實(shí)驗(yàn)代碼很簡(jiǎn)單,先通過(guò)setZeroTimeout
也就是postMessage
版本來(lái)遞歸計(jì)數(shù)到 100,然后切換成 setTimeout
計(jì)數(shù)到 100。
直接放結(jié)論,這個(gè)差距不固定,在 mac 上用無(wú)痕模式排除插件等因素的干擾后,以計(jì)數(shù)到 100 為例,大概有 80 ~ 100 倍的時(shí)間差距。在硬件更好的臺(tái)式機(jī)上,甚至能到 200 倍以上。
Performance 面板
只是看冷冰冰的數(shù)字還不夠過(guò)癮,打開(kāi) Performance 面板,看看更直觀的可視化界面中,postMessage
版的定時(shí)器和setTimeout
版的定時(shí)器是如何分布的。
這張分布圖非常直觀的體現(xiàn)出了上面所說(shuō)的所有現(xiàn)象,左邊的postMessage
版本的定時(shí)器分布非常密集,大概在 5ms 以內(nèi)就執(zhí)行完了所有的計(jì)數(shù)任務(wù)。
而右邊的setTimeout
版本相比較下分布的就很稀疏了,而且通過(guò)上方的時(shí)間軸可以看出,前四次的執(zhí)行間隔大概在 1ms 左右,到了第五次就拉開(kāi)到 4ms 以上。
作用
也許有同學(xué)會(huì)問(wèn),有什么場(chǎng)景需要無(wú)延遲的定時(shí)器?其實(shí)在 React 的源碼中,做時(shí)間切片的部分就用到了。
const channel = new MessageChannel(); const port = channel.port2; // 每次 port.postMessage() 調(diào)用就會(huì)添加一個(gè)宏任務(wù) // 該宏任務(wù)為調(diào)用 scheduler.scheduleTask 方法 channel.port1.onmessage = scheduler.scheduleTask; const scheduler = { scheduleTask() { // 挑選一個(gè)任務(wù)并執(zhí)行 const task = pickTask(); const continuousTask = task(); // 如果當(dāng)前任務(wù)未完成,則在下個(gè)宏任務(wù)繼續(xù)執(zhí)行 if (continuousTask) { port.postMessage(null); } }, };
React 把任務(wù)切分成很多片段,這樣就可以通過(guò)把任務(wù)交給postMessage
的回調(diào)函數(shù),來(lái)讓瀏覽器主線程拿回控制權(quán),進(jìn)行一些更優(yōu)先的渲染任務(wù)(比如用戶輸入)。
為什么不用執(zhí)行時(shí)機(jī)更靠前的微任務(wù)呢?關(guān)鍵的原因在于微任務(wù)會(huì)在渲染之前執(zhí)行,這樣就算瀏覽器有緊急的渲染任務(wù),也得等微任務(wù)執(zhí)行完才能渲染。
總結(jié)
可以了解如下幾個(gè)知識(shí)點(diǎn):文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-821445.html
-
setTimeout
的 4ms 延遲歷史原因,具體表現(xiàn)。 - 如何通過(guò)
postMessage
實(shí)現(xiàn)一個(gè)真正 0 延遲的定時(shí)器。 -
postMessage
定時(shí)器在 React 時(shí)間切片中的運(yùn)用。 - 為什么時(shí)間切片需要用宏任務(wù),而不是微任務(wù)。
本文轉(zhuǎn)載于:
https://juejin.cn/post/7229520942668824633
如果對(duì)您有所幫助,歡迎您點(diǎn)個(gè)關(guān)注,我會(huì)定時(shí)更新技術(shù)文檔,大家一起討論學(xué)習(xí),一起進(jìn)步。
?文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-821445.html
到了這里,關(guān)于記錄--你敢信?比 setTimeout 還快 80 倍的定時(shí)器的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!