js實現0ms延時定時器的幾種方式

福祿網路技術團隊發表於2021-07-27

這兩天看到一篇介紹《如何實現準時的 setTimeout?》的文章,文章起源於一道面試題:有什麼辦法讓setTimeout準時呀?具體文章內容可檢視附錄【1】,看完之後,引起了我對setTimeout這個函式的探究興趣,因此在MDN上重新查閱了相關文件,其中提到【最小延時 >=4ms】的一點,因此使用setTimeout不能實現0ms延時的定時器,如果要實現的話,提供了一個參考連結【2】,作者的實現思路是通過postMessage來模擬,繞過setTimeout的限制,從而實現0ms延時的定時器,說簡單來講就是起了一個巨集任務去執行回撥,先具體看下是怎麼實現的:

(function() {
	var timeouts = [];
	var messageName = "zero-timeout-message";
	// Like setTimeout, but only takes a function argument.  There's
	// no time argument (always zero) and no arguments (you have to use a closure)
	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);

	// Add the one thing we want added to the window object.
	window.setZeroTimeout = setZeroTimeout;
})();

作者還提供了一個demo頁面【3】,通過於setTimeout(0)進行對比,在我瀏覽器的執行結果如下:

100 iterations of setZeroTimeout took 15 milliseconds.
100 iterations of setTimeout(0) took 488 milliseconds.

根據結果對比來看,setZeroTimeout執行比setTimeout快了上百倍,這是一個巨大的提升。今天想討論的是除了上述這種方式,還可以通過哪些方式來實現一個0ms延時的定時器呢,首先,我們要確定一下我們自定義的定時器是非同步的,其次是儘可能早的被執行。說起非同步,js提供了好幾種解決方案,我們可以逐一去驗證。
在深入討論各種實現方式之前,約定提供的setTimeout對比版本如下,後面自定義實現的方案都將和setTimeout版本的執行時間進行對比,程式碼比較簡單:

(function() {
	let i = 0;
	const start = Date.now();
	function test() {
		if(i++ < 100) {
			setTimeout(test);
		} else {
			console.log('setTimeout執行時間:', Date.now() - start);
		}
	}
	setTimeout(test);
})();

queueMicrotask

queueMicrotask這個api可以新增一個微任務,使用比較簡單,直接傳遞一個回撥函式即可,具體實現如下:

(function() {
	function setZeroTimeout(fn) {
		queueMicrotask(fn);
	}
	let i = 0;
	const start = Date.now();
	function test() {
		if(i++ < 100) {
			setZeroTimeout(test);
		} else {
			console.log('setZeroTimeout執行時間:', Date.now() - start);
		}
	}
	setZeroTimeout(test);
})();

通過和setTimeout版本進行對比,最終結果如下:

setZeroTimeout執行時間: 2
setTimeout執行時間: 490

關於這個API的介紹在MDN上有詳細的說明,就不展開介紹了,這裡多說一點,根據規範文件的說明,大多數情況下,推薦使用requestAnimationFrame()和requestIdleCallback()等api,因為queueMicrotask會阻塞渲染,在很多時候都不是一種好的實踐。

async/await

async/await對於前端開發人員來說已經是必不可少的了,這裡我們也可以用來實現:

(function() {
	async function setAsyncTimeout(fn) {
		Promise.resolve().then(fn);
	}
	let i = 0;
	const start = Date.now();
	async function test() {
		if (i++ < 100) {
			await setAsyncTimeout(test);
		} else {
			console.log('setAsyncTimeout執行時間:', Date.now() - start);
		}
	}
	setAsyncTimeout(test);
})();

通過和setTimeout版本進行對比,最終結果如下:

setAsyncTimeout執行時間: 2
setTimeout執行時間: 490

如果不嫌麻煩,還可以通過Promise來實現,其實都是大同小異,無非多些點程式碼,這裡就省略了。

MessageChannel

MessageChannel允許我們建立一個新的訊息通道,並通過它的兩個MessagePort屬性傳送資料,MessageChannel提供埠的概念,實現埠之間的通訊,比如worker/iframe之間的通訊。

(function() {
	const channel = new MessageChannel();
	function setMessageChannelTimeout(fn) {
		channel.port2.postMessage(null);
	}
	channel.port1.onmessage = function() {
				test();
	};
	let i = 0;
	const start = Date.now();
	function test() {
		if(i++ < 100) {
			setMessageChannelTimeout(test);
		} else {
			console.log('setMessageChannelTimeout執行時間:', Date.now() - start);
		}
	}
	setMessageChannelTimeout(test);
})();

通過和setTimeout版本進行對比,最終結果如下:

setMessageChannelTimeout執行時間: 4
setTimeout執行時間: 490

第三種方式執行時間比前面兩種更長些,因為通過MessageChannel產生的是巨集任務,其他兩種是微任務,微任務執行靠前,且會阻塞主執行緒,因此時間會長一點。

最後

本文提供了三種實現方式,都是圍繞js提供非同步解決方案來實現的,實現本身並不複雜,如果讀者有其他實現方式,歡迎留言交流。
附錄
​​【1】https://mp.weixin.qq.com/s/QRIXBoKr2dMgLob3Atq9-g
【2】https://dbaron.org/log/20100309-faster-timeouts
【3】https://dbaron.org/mozilla/zero-timeout

福祿·研發中心 福袋

相關文章