天天看點

JavaScript 計時器

在 JavaScript 中,計時器是經常用到的,但是我們在使用計時器時如果不小心就會踩到坑中

說到計時器肯定時離不開 setTimeout 和 setInertval 這兩個函數的,這兩個函數的用法基本一緻,在使用時一般都要傳入兩個參數

第一個參數是一個回調函數,用于倒計時結束時調用;第二個參數是倒計時的時間 delay,第二個參數不寫的話預設為 0。這兩個函數的差別在于 setTimeout 隻倒計時一次,setInterval 會每隔 delay 這段時間就執行回調函數一次

接下來說說計時器裡面的一些坑

1.第一個坑

由于 JavaScript 是一門單線程的語言,它沒辦法同時執行多個任務,對于倒計時這種事件,JavaScript 解釋器中有一個用來存放回調函數的隊列,隻有解釋器中的同步事件都執行完了之後才會去隊列裡面将回調函數取出來執行

console.log(1);
setTimeout(() => {
    console.log(2);
}, 0);
console.log(3);           

比如上面的這段代碼,可以看到列印出來的結果是 1 3 2

這是因為 JS 解釋器會先執行同步的代碼,将 1 和 3 列印出來,然後再執行 setTimeout 中的回調函數,将 2 列印出來。是以雖然 setTimeout 中的延時為 0,但是還是在

console.log(3)

這句代碼執行完成之後才執行

對于 setTimeout 延時為 0 時的情況我們可以将它了解為盡快執行 setTimeout 中的代碼(和立即執行有所差別)

2.第二個坑

如果你給某個東西(通常是按鈕之類的)加上了觸發定時器的事件,那麼要記得每次在使用定時器之前要把之前的定時器清除掉,要不然你的定時器的速度會越來越快,這是因為你沒有清理掉之前的定時器,是以當你不斷點選按鈕時,定時器不斷的累加起來,這樣每個定時器的間隔就越來越小,導緻你的倒計時越來越快

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <button id="btn">開始計時</button>
    <div id="num">0</div>
</body>
<script>
    let num = 0;
    document.getElementById("btn").addEventListener("click", () => {
        setInterval(() => {
        num++;
        document.getElementById('num').innerText = num;
    }, 1000);
    })
</script>
</html>           

在上面的這段代碼中,當我們點選“開始計時”按鈕之後,頁面中的計時就啟動了,但是如果你多次點選它的話,你會發現計時會越來越快

要解決這個問題就需要我們在開啟一個新的計時器開啟之前将已經開啟了的計時器清除掉

let num = 0;
let timerId;
document.getElementById("btn").addEventListener("click", () => {
    clearInterval(timerId);
    timerId = setInterval(() => {
        hasTimer = true;
        num++;
        document.getElementById('num').innerText = num;
}, 1000);
})           

如上面代碼所示,我們将開啟新的計時器前的計時器清除掉,這樣當使用者重複點選按鈕時,就不會出現速度越來越快的問題

3.第三個坑

我們讀取的時間不能從使用者的用戶端讀取,而是應該從服務端那邊擷取。如果從用戶端直接讀取的話,不同使用者的用戶端之間的時間可能存在誤差,同時可能存在使用者直接修改用戶端的時間來改變你的倒計時

是以做倒計時相關的功能的時候我們的倒計時所開始的時間需要從服務端那邊擷取,但是從服務端那邊擷取到時間到倒計時顯示在頁面是有一定的時間差的

用戶端從服務端擷取到用戶端将倒計時顯示出來的過程大概如下

用戶端發送 HTTP 請求給服務端 -> 服務響應并将時間發送給用戶端 -> 用戶端根據這個時間将倒計時顯示在頁面上

在這個過程中,是有一定的時間損耗的,比如資料請求和響應的過程,以及另外一個對倒計時影響更大的東西———— JavaScript 解釋器會先執行同步的代碼,執行完同步的代碼之後再執行異步的代碼

如果同步代碼的執行時間太長的話,倒計時的精準度就下降了很多,

let start = new Date().getTime(); 
let count = 0; 
// 模拟執行大量代碼
setInterval(function(){ 
    var i = 0; 
    while(i++ < 100000000); 
}, 0); 
// 倒計時
setInterval(function(){ 
    count++; 
    console.log(new Date().getTime() - (start + count * 1000)); 
},1000);           

上面的這段代碼的執行效果如下圖所示,可以看到如果有大量的代碼執行,那麼倒計時的精準度就會下降很多,是以我們需要找一個方法來解決這個問題

JavaScript 計時器

要解決倒計時精準度的問題,我們可以将目前的時間與上一次倒計時開始的時間的內插補點與 delay 做對比,将 delay 減去這個內插補點進而得到一個校準後的時間,将這個時間做為下一次倒計時的時間,代碼如下面所示

// 模拟執行大量代碼
setInterval(() => { 
    let i = 0; 
    while(i++ < 100000000); 
}, 0); 
// 倒計時
let delay = 1000, count = 0, startTime = new Date().getTime(),
    ms = 100000 // 要倒計時的時間,一百秒
if( ms >= 0){
    let timeCounter = setTimeout(countDown, delay);
}
function countDown(){
    count++;
    let offset = new Date().getTime() - (startTime + count * delay);
    let nextTime = delay - offset;
    if (nextTime < 0) { nextTime = 0 };
    ms -= delay;
    console.log("誤差:" + offset + "ms,下一次執行:" + nextTime + "ms後,離活動開始還有:" + ms + "ms");
    if(ms < 0){
        clearTimeout(timeCounter);
    }else{
        timeCounter = setTimeout(countDown,nextTime);
    }
}           

通過上一次倒計時開始的時間和目前時間的對比,我們可以将目前應該倒計時的時間算出來,這樣就可以使我們的倒計時盡可能精準

繼續閱讀