前言
setTimeout()
我們?cè)谌粘9ぷ髦薪?jīng)常使用,最近做了一個(gè)功能是關(guān)于setTimeout()
的,總結(jié)了一些用法。
在這篇文章中,你將了解 setTimeout()
方法——它是什么以及如何在你的程序中使用它。
以下是我在這篇快速指南中要介紹的內(nèi)容:
- JavaScript 中的
setTimeout()
基本語(yǔ)法 - 進(jìn)階語(yǔ)法:防抖、代碼邏輯執(zhí)行時(shí)間可能比定時(shí)器時(shí)間間隔要長(zhǎng)如何處理
setTimeout()
的定時(shí)器是否精準(zhǔn)- 定時(shí)器在非激活tab或者熄屏的時(shí)候還會(huì)按照預(yù)期去執(zhí)行嗎
- 定時(shí)器如何進(jìn)行時(shí)間糾正
- 簡(jiǎn)單比較下scheduler.yield
基本的語(yǔ)法
scss
代碼解讀
復(fù)制代碼setTimeout(code)
setTimeout(code, delay)
setTimeout(functionRef)
setTimeout(functionRef, delay)
setTimeout(functionRef, delay, param1)
setTimeout(functionRef, delay, param1, param2)
setTimeout(functionRef, delay, param1, param2, /* …, */ paramN)
例如我們寫個(gè)最簡(jiǎn)單的
我們來(lái)添加一個(gè)參數(shù)進(jìn)行測(cè)試 如下,可以打印出來(lái)獲取到的參數(shù)params1和params2
返回值
返回值 timeoutID
是一個(gè)正整數(shù),表示由 setTimeout()
調(diào)用創(chuàng)建的定時(shí)器的標(biāo)識(shí)符??梢詫⑦@個(gè)值傳遞給 clearTimeout()
來(lái)取消該定時(shí)器。
我們嘗試在1秒的時(shí)候?qū)imerId進(jìn)行清除,測(cè)試下是否可以正常打印,如你所料,不會(huì)打印timerId內(nèi)的數(shù)據(jù)
進(jìn)階用法
1 防抖(常用于表單提交)
我們?cè)诒韱翁峤坏臅r(shí)候,比如希望表單的提交按鈕在1秒內(nèi)之內(nèi)只生效一次,可以利用settimeout來(lái)實(shí)現(xiàn)。
如下,假如在頁(yè)面上有一個(gè)id為submitBtn的按鈕,添加了一個(gè)點(diǎn)擊事件,當(dāng)1秒內(nèi)每次點(diǎn)擊都會(huì)清除之前的timeoutId,不會(huì)執(zhí)行提交的邏輯。從而確保只有在最后一次點(diǎn)擊后的1秒內(nèi)沒(méi)有再次點(diǎn)擊時(shí),才會(huì)執(zhí)行實(shí)際的操作。
javascript
代碼解讀
復(fù)制代碼 let submitBtn = document.getElementById("submitBtn");
let timeoutId;
function onTest() {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(function() {
// 在這里編寫提交邏輯
alert("按鈕被點(diǎn)擊了!");
}, 1000);
}
submitBtn.addEventListener("click", onTest);
2 優(yōu)先展示用戶希望看到的內(nèi)容(利用JS事件機(jī)制)
假如我們有一個(gè)人員的新增表單,部門樹(shù)的下拉框有10K條數(shù)據(jù),假如我們還沒(méi)有虛擬下拉樹(shù),數(shù)據(jù)的渲染會(huì)很慢, 打開(kāi)這個(gè)新增的表單可能會(huì)有3到5S的延遲后表單才會(huì)打開(kāi)。
我們可以將這個(gè)部門樹(shù)的下拉值綁定寫在了settimeout內(nèi),等主線程的任務(wù)執(zhí)行后,再執(zhí)行綁定部門樹(shù)的操作,可以優(yōu)先將其他的內(nèi)容展示出來(lái)后再進(jìn)行部門樹(shù)的綁定。
3 替代setInterval防止請(qǐng)求阻塞
比如我們有個(gè)需求: 1秒的間隔輪詢服務(wù)器,頁(yè)面展示內(nèi)容
正常的話可以用setInterval沒(méi)什么問(wèn)題,
假如請(qǐng)求的接口可能因網(wǎng)絡(luò)延遲、服務(wù)器無(wú)響應(yīng)以及許多其他的問(wèn)題而導(dǎo)致請(qǐng)求無(wú)法在分配的時(shí)間內(nèi)完成(假如服務(wù)器處理了3秒)
我們把請(qǐng)求的間隔設(shè)置為1秒,實(shí)際的接口在2秒多,就會(huì)造成請(qǐng)求的阻塞,其實(shí)我們想實(shí)現(xiàn)的是在一次接口請(qǐng)求結(jié)束后再發(fā)起下一次請(qǐng)求
如下方的截圖
接口固定在大約2秒返回,就會(huì)導(dǎo)致永遠(yuǎn)有幾個(gè)請(qǐng)求在阻塞,跟我們的預(yù)想不同
我們對(duì)上面的代碼進(jìn)行改造如下
實(shí)現(xiàn)如果如下,我們保證有一個(gè)請(qǐng)求在pending,會(huì)在一個(gè)請(qǐng)求發(fā)出后,再進(jìn)行發(fā)送下一次請(qǐng)求
代碼解釋如下
在上面的代碼片段中,聲明了一個(gè)具名函數(shù) loop()
,并被立即執(zhí)行。loop()
在完成代碼邏輯的執(zhí)行后,會(huì)在內(nèi)部遞歸調(diào)用 setTimeout()
。雖然該模式不保證以固定的時(shí)間間隔執(zhí)行,但它保證了上一次定時(shí)任務(wù)在遞歸前已經(jīng)完成。
可能遇到的場(chǎng)景
1 settimeout這個(gè)定時(shí)器準(zhǔn)嗎
答案:正常場(chǎng)景下是準(zhǔn)的,某些場(chǎng)景下是不準(zhǔn)的,
有很多因素會(huì)導(dǎo)致超時(shí)比設(shè)定的預(yù)期值更久 如需查看更多的原因點(diǎn)擊我
嵌套多次的時(shí)候會(huì)有4ms的延遲(可以點(diǎn)擊下方的例子進(jìn)行測(cè)試)
當(dāng)嵌套多次的結(jié)果如下,就不做詳細(xì)介紹了,了解就行。目前我沒(méi)想到哪些場(chǎng)景下settimeout會(huì)被嵌套調(diào)用5次,就不做繼續(xù)研究,我們只需要知道這個(gè)概念就行,等出現(xiàn)這個(gè)問(wèn)題了有個(gè)排查方向就行
主線程有耗時(shí)的任務(wù)
我們做個(gè)實(shí)驗(yàn),我們對(duì)id為main對(duì)dom進(jìn)行2萬(wàn)次的修改innerHtml,在控制臺(tái)打印時(shí)間,
原本2秒變?yōu)榱?秒,所以settimeout 有時(shí)候并不會(huì)按照預(yù)期的時(shí)間間隔來(lái)執(zhí)行
這里其實(shí)就涉及到了js的事件為單線程機(jī)制,我們用performance簡(jiǎn)單分析下,看到有個(gè)3865ms的parse HTML的,然后執(zhí)行Run Microtasks也就是微任務(wù),也就是等我們的同步任務(wù)執(zhí)行后,再去執(zhí)行settimeout內(nèi)的東西
在非活動(dòng)標(biāo)簽的tab,待機(jī)下的settimeout還會(huì)按照預(yù)期執(zhí)行嗎?
以下的測(cè)試是在chrome的mac版本進(jìn)行測(cè)試
代碼很簡(jiǎn)單,寫了個(gè)2秒的定時(shí)器
待機(jī)狀態(tài)
tab非激活
過(guò)幾分鐘后,定時(shí)器會(huì)從2秒變?yōu)?分鐘
由此我們得知,在瀏覽器激活的時(shí)候,settimeout 會(huì)按照我們的預(yù)期去執(zhí)行,在非激活(tab不選中過(guò)段時(shí)間、電腦處于待機(jī)狀態(tài))下,定時(shí)器會(huì)按照1min一次去執(zhí)行,
但是有種特殊場(chǎng)景,audio假如正在播放,此時(shí)頁(yè)面的settimeout會(huì)被當(dāng)作激活狀態(tài)
settimeout 時(shí)間糾正
當(dāng)然,因?yàn)閖s的事件機(jī)制,settimeout存在時(shí)間偏差,就會(huì)存在時(shí)間糾正,下面介紹了兩個(gè)js的時(shí)間糾正方式,雖然我試用下來(lái)不太理想,也可能是我的姿勢(shì)不對(duì),有更好的方式歡迎和我討論
計(jì)算時(shí)間差(并不能完全避免,只能糾正)
直接在網(wǎng)上抄寫了個(gè)例子。 使用 setTimeout 進(jìn)行計(jì)時(shí),每次計(jì)時(shí)都會(huì)用系統(tǒng)時(shí)間修復(fù)時(shí)間差
js
代碼解讀
復(fù)制代碼 function timer_setTimeout() {
const speed = 1000; // 設(shè)置定時(shí)器的間隔速度為1000毫秒(1秒)
let countTime = 0; // 初始化計(jì)時(shí)器計(jì)數(shù)變量
let start = new Date().getTime(); // 記錄計(jì)時(shí)開(kāi)始時(shí)的時(shí)間戳
// 定義計(jì)時(shí)器的執(zhí)行函數(shù)
function run() {
countTime++; // 每次執(zhí)行時(shí)遞增計(jì)時(shí)器的計(jì)數(shù)
// 計(jì)算按照計(jì)時(shí)器當(dāng)前速度實(shí)際經(jīng)過(guò)的時(shí)間(countTime * 速度)
let realTime = (countTime * speed);
// 計(jì)算從計(jì)時(shí)開(kāi)始到現(xiàn)在系統(tǒng)經(jīng)過(guò)的時(shí)間
let sysTime = (Date.now() - start);
// 計(jì)算實(shí)際時(shí)間和理想時(shí)間之間的差異
let patch = (sysTime - realTime);
// 使用系統(tǒng)時(shí)間進(jìn)行修復(fù),調(diào)整下一次setTimeout的延遲時(shí)間
// 通過(guò)設(shè)置speed - diff,嘗試校正setTimeout的延遲,以補(bǔ)償偏差
window.setTimeout(run, (speed - patch));
// 更新頁(yè)面元素timeoutDom的文本內(nèi)容,顯示當(dāng)前計(jì)時(shí)器的值
timeoutDom.innerText = `setTimeout: ${countTime}`;
}
// 啟動(dòng)計(jì)時(shí)器,初始調(diào)用run函數(shù),并設(shè)置延遲為speed
window.setTimeout(run, speed);
}
// 調(diào)用函數(shù),創(chuàng)建并啟動(dòng)setTimeout計(jì)時(shí)器
timer_setTimeout();
webworker(測(cè)試下來(lái)效果不好)
javascript
代碼解讀
復(fù)制代碼 console.log(new Date());
for (let index = 0; index < 20000; index++) {
document.getElementById('main').innerHTML += index;
}
function timer_worker() {
// 創(chuàng)建一個(gè)Blob對(duì)象,用于生成一個(gè)可以在Web Worker中執(zhí)行的JavaScript代碼URL
const blob = new Blob(
[
`let countTime = 0;
self.setInterval(() => { // 在Web Worker的上下文中設(shè)置一個(gè)定時(shí)器
countTime++; // 每次定時(shí)器觸發(fā)時(shí)遞增計(jì)數(shù)器
self.postMessage(countTime); // 使用postMessage方法發(fā)送計(jì)數(shù)器的值到主線程
}, 5000);` // 定時(shí)器的時(shí)間間隔設(shè)置為1秒
],
{ type: 'application/javascript' }
); // 指定Blob的內(nèi)容類型為JavaScript
// 使用URL.createObjectURL方法創(chuàng)建一個(gè)可以被Web Worker使用的URL
const worker = new Worker(URL.createObjectURL(blob));
// 設(shè)置Web Worker的onmessage事件處理器
// 當(dāng)Web Worker使用postMessage發(fā)送消息時(shí),該處理器會(huì)被觸發(fā)
worker.onmessage = (ev) => {
// 更新DOM元素workerDom的文本內(nèi)容,顯示從Web Worker接收到的計(jì)時(shí)器值
console.log(new Date() + ` worker: ${ev.data}`);
};
}
timer_worker();
遇到長(zhǎng)的任務(wù)的時(shí)候,還是會(huì)延遲執(zhí)行
scheduler.yield
在查找 setTimeout
的資料的時(shí)候,發(fā)現(xiàn)了有一個(gè)比較好的東西,scheduler.yield
, 感興趣的老板可以點(diǎn)擊我進(jìn)行體驗(yàn),目前兼容性不好(24年8月21日的才支持),不做詳細(xì)介紹。
兼容性如下
總結(jié)一句話
使用 setTimeout
是將事件插入在隊(duì)列的結(jié)尾。yield而是發(fā)送到隊(duì)列的前面。這樣,既可以讓步以提高網(wǎng)站上的輸入響應(yīng)速度,又可以確保在讓步后完成的工作不會(huì)延遲。
參考文檔
- developer.chrome.com/blog/introd…
- developer.chrome.com/blog/timer-…
- developer.mozilla.org/zh-CN/docs/…
總結(jié)
setTimeout
可以實(shí)現(xiàn)延遲的一些事件,比如多少秒后返回首頁(yè)等setTimeout
可以通過(guò)一定時(shí)間內(nèi)事件只執(zhí)行一次實(shí)現(xiàn)防抖setTimeout
可以替代setInterval
實(shí)現(xiàn)更好的輪詢setTimeout
可以通過(guò)js事件的異步機(jī)制,優(yōu)先展示用戶關(guān)注的內(nèi)容setTimeout
的事件定時(shí)器在某些場(chǎng)景下定時(shí)器不準(zhǔn),大部分場(chǎng)景下是準(zhǔn)的,如果要求比較精確需要可以進(jìn)行時(shí)間差的修正setTimeout
在熄屏或者非激活tab的場(chǎng)景下,計(jì)時(shí)器會(huì)延長(zhǎng)變?yōu)?min執(zhí)行一次setTimeout
是把事件插入到尾部,未來(lái)我們可以使用scheduler.yield
的暫停頁(yè)面的操作,提升用戶體驗(yàn) 、