如何實現(xiàn)比 setTimeout 快 80 倍的定時器呀袱?
在瀏覽器中,setTimeout()/setInterval() 的每調(diào)用一次定時器的最小間隔是 4ms郑叠,這通常是由于函數(shù)嵌套導(dǎo)致(嵌套層級達到一定深度)压鉴。
簡單來說,5 層以上的定時器嵌套會導(dǎo)致至少 4ms 的延遲锻拘。
用如下代碼做個測試:
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);
// 1.3500000350177288
// 1.244999933987856
// 1.38000026345253
// 1.2050000950694084
// 4.724999889731407
// 5.309999920427799
探索
假設(shè)我們就需要一個「立刻執(zhí)行」的定時器呢?有什么辦法繞過這個 4ms 的延遲嗎击蹲,上面那篇 MDN 文檔的角落里有一些線索:
如果想在瀏覽器中實現(xiàn) 0ms 延時的定時器署拟,你可以參考這里[3]所說的
window.postMessage()
。
用 postMessage
來實現(xiàn)真正 0 延遲的定時器:
(function() {
var timeouts = [];
var messageName = 'zero-timeout-message';
// 保持 setTimeout 的形態(tài)歌豺,只接受單個函數(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 對象上
window.setZeroTimeout = setZeroTimeout;
})();
由于 postMessage
的回調(diào)函數(shù)的執(zhí)行時機和 setTimeout
類似类咧,都屬于宏任務(wù)馒铃,所以可以簡單利用 postMessage
和 addEventListener('message')
的消息通知組合蟹腾,來實現(xiàn)模擬定時器的功能。
再利用上面的嵌套定時器的例子來跑一下測試:
// 0.3850003704428673
// 0.23999996483325958
// 0.15999982133507729
// 0.3349999897181988
// 0.169999897480011
// 0.135000329464674
全部在 0.1 ~ 0.3 毫秒級別区宇,而且不會隨著嵌套層數(shù)的增多而增加延遲娃殖。
測試
從理論上來說,由于 postMessage
的實現(xiàn)沒有被瀏覽器引擎限制速度议谷,一定是比 setTimeout
要快的炉爆。但空口無憑,咱們用數(shù)據(jù)說話卧晓。
作者設(shè)計了一個實驗方法芬首,就是分別用 postMessage
版定時器和傳統(tǒng)定時器做一個遞歸執(zhí)行計數(shù)函數(shù)的操作,看看同樣計數(shù)到 100 分別需要花多少時間逼裆。讀者也可以在這里自己跑一下測試[4]郁稍。
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();
// 通過遞歸 setZeroTimeout 達到 100 計數(shù)
// 達到 100 后切換成 setTimeout 來實驗
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);
// 通過遞歸 setTimeout 達到 100 計數(shù)
function test2() {
if (++i == 100) {
var endTime = Date.now();
printOutput(
'100 iterations of setTimeout(0) took ' + (endTime - startTime) + ' milliseconds.'
);
} else {
setTimeout(test2, 0);
}
}
}
直接放結(jié)論,這個差距不固定胜宇,在我的 mac 上用無痕模式排除插件等因素的干擾后耀怜,以計數(shù)到 100 為例,大概有 80 ~ 100 倍的時間差距掸屡。在我硬件更好的臺式機上封寞,甚至能到 200 倍以上。
Performance 面板
只是看冷冰冰的數(shù)字還不夠過癮仅财,我們打開 Performance 面板狈究,看看更直觀的可視化界面中,postMessage 版的定時器和 setTimeout 版的定時器是如何分布的盏求。
這張分布圖非常直觀的體現(xiàn)出了我們上面所說的所有現(xiàn)象抖锥,左邊的 postMessage 版本的定時器分布非常密集,大概在 5ms 以內(nèi)就執(zhí)行完了所有的計數(shù)任務(wù)碎罚。
而右邊的 setTimeout 版本相比較下分布的就很稀疏了磅废,而且通過上方的時間軸可以看出,前四次的執(zhí)行間隔大概在 1ms 左右荆烈,到了第五次就拉開到 4ms 以上拯勉。
總結(jié)
通過本文,你大概可以了解如下幾個知識點:
- setTimeout 的 4ms 延遲歷史原因憔购,具體表現(xiàn)宫峦。
- 如何通過 postMessage 實現(xiàn)一個真正 0 延遲的定時器。