很長時間以來,定時器一直是javascript動畫的核心技術。可是,關於定時器,人們一般只瞭解如何使用setTimeout()和setInterval(),對它們的內在運行機制並不理解,對於與預想不一樣的實際運行情況也沒法解決。本文將詳細介紹定時器的相關內容javascript
setTimeout()方法用來指定某個函數或字符串在指定的毫秒數以後執行。它返回一個整數,表示定時器的編號,這個值能夠傳遞給clearTimeout()用於取消這個函數的執行html
如下代碼中,控制檯先輸出0,大概過1000ms即1s後,輸出定時器setTimeout()方法的返回值1java
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-瀏覽器的函數傳參this
setTimeout((function(a,b){ return function(){ console.log(a+b); } })(1,1),1000);
或者將函數寫在定時器外面,而後函數在定時器中的匿名函數中帶參數調用spa
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
//控制檯輸出一、二、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的用法與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()時,僅當沒有該定時器的任何其餘代碼實例時,纔將定時器代碼添加到隊列中。這確保了定時器代碼加入到隊列中的最小時間間隔爲指定間隔
可是,這樣會致使兩個問題:一、某些間隔被跳過;二、多個定時器的代碼執行之間的間隔可能比預期的小
假設,某個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>