1. 程式人生 > >深入理解定時器系列第一篇——理解setTimeout和setInterval

深入理解定時器系列第一篇——理解setTimeout和setInterval

前面的話

  很長時間以來,定時器一直是javascript動畫的核心技術。但是,關於定時器,人們通常只瞭解如何使用setTimeout()和setInterval(),對它們的內在執行機制並不理解,對於與預想不同的實際執行狀況也無法解決。本文將詳細介紹定時器的相關內容

setTimeout()

  setTimeout()方法用來指定某個函式或字串在指定的毫秒數之後執行。它返回一個整數,表示定時器的編號,這個值可以傳遞給clearTimeout()用於取消這個函式的執行

  以下程式碼中,控制檯先輸出0,大概過1000ms即1s後,輸出定時器setTimeout()方法的返回值1

var
Timer = setTimeout(function(){ console.log(Timer); },1000); console.log(0);

  也可以寫成字串引數的形式,由於這種形式會造成javascript引擎兩次解析,降低效能,故不建議使用

var Timer = setTimeout('console.log(Timer);',1000);
console.log(0);

  如果省略setTimeout的第二個引數,則該引數預設為0

  以下程式碼中,控制檯出現0和1,但是0卻在前面,後面會解釋這個疑問

var Timer = setTimeout(function
(){ console.log(Timer); }); console.log(0);

  實際上,除了前兩個引數,setTimeout()方法還允許新增更多的引數,它們將被傳入定時器中的函式中

  以下程式碼中,控制檯大概過1000ms即1s後,輸出2,而IE9-瀏覽器只允許setTimeout有兩個引數,不支援更多的引數,會在控制檯輸出NaN

setTimeout(function(a,b){
  console.log(a+b);
},1000,1,1);

  可以使用IIFE傳參來相容IE9-瀏覽器的函式傳參

setTimeout((function(a,b){
    
return function(){ console.log(a+b); } })(1,1),1000);

  或者將函式寫在定時器外面,然後函式在定時器中的匿名函式中帶引數呼叫

function test(a,b){
    console.log(a+b);
}
setTimeout(function(){
    test(1,1);
},1000);

  [注意]IE8-瀏覽器不允許向定時器中傳遞事件物件event,如果要使用事件物件中的某些屬性,可以將其儲存在變數中傳遞進去

div.onclick = function(e){
    e = e || event;
    var type = e.type;
    setTimeout(function(){
        console.log(type);//click
        console.log(e.type);//報錯
    })
}

this指向

  在this機制系列已經詳細介紹過this指向的4種繫結規則,由於定時器中的this存在隱式丟失的情況,且極易出錯,因此在這裡再次進行說明

var a = 0;
function foo(){
    console.log(this.a);
};
var obj = {
    a : 2,
    foo:foo
}
setTimeout(obj.foo,100);//0
//等價於
var a = 0;
setTimeout(function foo(){
    console.log(this.a);
},100);//0

  若想獲得obj物件中的a屬性值,可以將obj.foo函式放置在定時器中的匿名函式中進行隱式繫結

var a = 0;
function foo(){
    console.log(this.a);
};
var obj = {
    a : 2,
    foo:foo
}
setTimeout(function(){
    obj.foo();
},100);//2

  或者也可以使用bind方法將foo()方法的this繫結到obj上

var a = 0;
function foo(){
    console.log(this.a);
};
var obj = {
    a : 2,
    foo:foo
}
setTimeout(obj.foo.bind(obj),100);//2

clearTimeout()

  setTimeout函式返回一個表示計數器編號的整數值,將該整數傳入clearTimeout函式,取消對應的定時器

//過100ms後,控制檯輸出setTimeout()方法的返回值1
var Timer = setTimeout(function(){
    console.log(Timer);
},100);

  於是可以利用這個值來取消對應的定時器

var Timer = setTimeout(function(){
    console.log(Timer);
},100);
clearTimeout(Timer);

  或者直接使用返回值作為引數

var Timer = setTimeout(function(){
    console.log(Timer);
},100);
clearTimeout(1);

  一般來說,setTimeout返回的整數值是連續的,也就是說,第二個setTimeout方法返回的整數值比第一個的整數值大1

//控制檯輸出1、2、3
var Timer1 = setTimeout(function(){
    console.log(Timer1);
},100);
var Timer2 = setTimeout(function(){
    console.log(Timer2);
},100);
var Timer3 = setTimeout(function(){
    console.log(Timer3);
},100);

setInterval()

  setInterval的用法與setTimeout完全一致,區別僅僅在於setInterval指定某個任務每隔一段時間就執行一次,也就是無限次的定時執行

<button id="btn">0</button>
<script>
var timer = setInterval(function(){
    btn.innerHTML = Number(btn.innerHTML) + 1;
},1000);
btn.onclick = function(){
    clearInterval(timer);
    btn.innerHTML = 0;
}
</script>

  [注意]HTML5標準規定,setTimeout的最短時間間隔是4毫秒;setInterval的最短間隔時間是10毫秒,也就是說,小於10毫秒的時間間隔會被調整到10毫秒

  大多數電腦顯示器的重新整理頻率是60HZ,大概相當於每秒鐘重繪60次。因此,最平滑的動畫效的最佳迴圈間隔是1000ms/60,約等於16.6ms

  為了節電,對於那些不處於當前視窗的頁面,瀏覽器會將時間間隔擴大到1000毫秒。另外,如果膝上型電腦處於電池供電狀態,Chrome和IE10+瀏覽器,會將時間間隔切換到系統定時器,大約是16.6毫秒

執行機制

  下面來解釋前面部分遺留的疑問,為什麼下面程式碼的控制檯結果中,0出現在1的前面呢?

setTimeout(function(){
    console.log(1);
});
console.log(0);

  實際上,把setTimeout的第二個引數設定為0s,並不是立即執行函式的意思,只是把函式放入非同步佇列。瀏覽器先執行完同步佇列裡的任務,才會去執行非同步佇列中的任務

  在下面這個例子中,給一個按鈕btn設定了一個事件處理程式。事件處理程式設定了一個250ms後呼叫的定時器。點選該按鈕後,首先將onclick事件處理程式加入佇列。該程式執行後才設定定時器,再有250ms後,指定的程式碼才被新增到佇列中等待執行

btn.onclick = function(){
    setTimeout(function(){
        console.log(1);
    },250);
}

  如果上面程式碼中的onclick事件處理程式執行了300ms,那麼定時器的程式碼至少要在定時器設定之後的300ms後才會被執行。佇列中所有的程式碼都要等到javascript程序空閒之後才能執行,而不管它們是如何新增到佇列中的

  如圖所示,儘管在255ms處添加了定時器程式碼,但這時候還不能執行,因為onclick事件處理程式仍在執行。定時器程式碼最早能執行的時機是在300ms處,即onclick事件處理程式結束之後

setInterval()的問題

  使用setInterval()的問題在於,定時器程式碼可能在程式碼再次被新增到佇列之前還沒有完成執行,結果導致定時器程式碼連續執行好幾次,而之間沒有任何停頓。而javascript引擎對這個問題的解決是:當使用setInterval()時,僅當沒有該定時器的任何其他程式碼例項時,才將定時器程式碼新增到佇列中。這確保了定時器程式碼加入到佇列中的最小時間間隔為指定間隔

  但是,這樣會導致兩個問題:1、某些間隔被跳過;2、多個定時器的程式碼執行之間的間隔可能比預期的小

  假設,某個onclick事件處理程式使用setInterval()設定了200ms間隔的定時器。如果事件處理程式花了300ms多一點時間完成,同時定時器程式碼也花了差不多的時間,就會同時出現跳過某間隔的情況

  例子中的第一個定時器是在205ms處新增到佇列中的,但是直到過了300ms處才能執行。當執行這個定時器程式碼時,在405ms處又給佇列添加了另一個副本。在下一個間隔,即605ms處,第一個定時器程式碼仍在執行,同時在佇列中已經有了一個定時器程式碼的例項。結果是,在這個時間點上的定時器程式碼不會被新增到佇列中

迭代setTimeout

  為了避免setInterval()定時器的問題,可以使用鏈式setTimeout()呼叫

setTimeout(function fn(){
    setTimeout(fn,interval);
},interval);

  這個模式鏈式呼叫了setTimeout(),每次函式執行的時候都會建立一個新的定時器。第二個setTimeout()呼叫當前執行的函式,併為其設定另外一個定時器。這樣做的好處是,在前一個定時器程式碼執行完之前,不會向佇列插入新的定時器程式碼,確保不會有任何缺失的間隔。而且,它可以保證在下一次定時器程式碼執行之前,至少要等待指定的間隔,避免了連續的執行

  使用setInterval()

<div id="myDiv" style="height: 100px;width: 100px;background-color: pink;position:absolute;left:0;"></div>
<script>
myDiv.onclick = function(){
    var timer = setInterval(function(){
        if(parseInt(myDiv.style.left) > 200){
            clearInterval(timer);
            return false;
        }
        myDiv.style.left = parseInt(myDiv.style.left) + 5 + 'px';    

    },16);    
}
</script>

   使用鏈式setTimeout()

<div id="myDiv" style="height: 100px;width: 100px;background-color: pink;position:absolute;left:0;"></div>
<script>
myDiv.onclick = function(){
    setTimeout(function fn(){
        if(parseInt(myDiv.style.left) <= 200){
            setTimeout(fn,16);    
        }else{
            return false;
        }
        myDiv.style.left = parseInt(myDiv.style.left) + 5 + 'px';    
    },16);    
}
</script>

應用

  使用定時器來調整事件發生順序

  【1】網頁開發中,某個事件先發生在子元素,然後冒泡到父元素,即子元素的事件回撥函式,會早於父元素的事件回撥函式觸發。如果,我們先讓父元素的事件回撥函式先發生,就要用到setTimeout(f, 0)

  正常情況下,點選div元素,先彈出0,再彈出1

<div id="myDiv" style="height: 100px;width: 100px;background-color: pink;"></div>
<script>
myDiv.onclick = function(){
    alert(0);
}
document.onclick = function(){
    alert(1);
}
</script>

  如果進行想讓document的onclick事件先發生,即點選div元素,先彈出1,再彈出0。則進行如下設定

<div id="myDiv" style="height: 100px;width: 100px;background-color: pink;"></div>
<script>
myDiv.onclick = function(){
    setTimeout(function(){
        alert(0);
    })
}
document.onclick = function(){
    alert(1);
}
</script>

  【2】使用者自定義的回撥函式,通常在瀏覽器的預設動作之前觸發。比如,使用者在輸入框輸入文字,keypress事件會在瀏覽器接收文字之前觸發。因此,下面的回撥函式是達不到目的

<input type="text" id="myInput">
<script>
myInput.onkeypress = function(event) {
  this.value = this.value.toUpperCase();
}
</script>

  上面程式碼想在使用者輸入文字後,立即將字元轉為大寫。但是實際上,它只能將上一個字元轉為大寫,因為瀏覽器此時還沒接收到文字,所以this.value取不到最新輸入的那個字元

  只有用setTimeout改寫,上面的程式碼才能發揮作用

<input type="text" id="myInput">
<script>
myInput.onkeypress = function(event) {
    setTimeout(function(){
        myInput.value = myInput.value.toUpperCase();
    });
}
</script>