js實現0ms延時定時器的幾種方式
這兩天看到一篇介紹《如何實現準時的 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
到此這篇關於js實現0ms延時定時器的幾種方式的文章就介紹到這瞭,更多相關js 延時定時器 內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- JS時間分片技術解決長任務導致的頁面卡頓
- 一篇文章帶你瞭解vue.js的事件循環機制
- 一篇文章讓你搞清楚JavaScript事件循環
- Nodejs新特性async和await的使用詳解
- es7中的async、await使用方法示例詳解